diff --git a/.gitignore b/.gitignore index 0fca5d50af..2927e3781f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ genhtml/ target/ *.lcov *.profraw +*.profdata /report flamegraph.html perf.data diff --git a/NOTICE b/NOTICE index 75eec1ac0c..66049053fe 100644 --- a/NOTICE +++ b/NOTICE @@ -279,7 +279,7 @@ with this software. If not, see ======================================================================= -'src/flamenco/nanopb' is currently a copy of the Nanopb library at +'src/ballet/nanopb' is currently a copy of the Nanopb library at https://jpa.kapsi.fi/nanopb/ (imported ca 2023-Nov). Copyright (c) 2011 Petteri Aimonen @@ -466,3 +466,37 @@ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +======================================================================= + +The fd_h2 library (src/waltz/h2) includes a modified version of the +HPACK Huffman decoder from nghttp2 (circa 2025-Mar). Original sources: +- https://github.com/nghttp2/nghttp2/blob/master/lib/nghttp2_hd.h +- https://github.com/nghttp2/nghttp2/blob/master/lib/nghttp2_hd_huffman.h +- https://github.com/nghttp2/nghttp2/blob/master/lib/nghttp2_hd_huffman.c +- https://github.com/nghttp2/nghttp2/blob/master/lib/nghttp2_hd_huffman_data.c + +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ diff --git a/book/api/metrics-generated.md b/book/api/metrics-generated.md index b9a94a06fb..2485d5f94f 100644 --- a/book/api/metrics-generated.md +++ b/book/api/metrics-generated.md @@ -149,6 +149,16 @@ | bundle_​transaction_​received | `counter` | Total count of transactions received, including transactions within bundles | | bundle_​packet_​received | `counter` | Total count of packets received | | bundle_​bundle_​received | `counter` | Total count of bundles received | +| bundle_​errors_​protobuf | `counter` | Number of gRPC errors encountered (Protobuf decode/encode error) | +| bundle_​errors_​transport | `counter` | Number of gRPC errors encountered (Transport error) | +| bundle_​errors_​timeout | `counter` | Number of gRPC errors encountered (I/O timeout) | +| bundle_​errors_​no_​fee_​info | `counter` | Number of gRPC errors encountered (Bundle dropped due to missing fee info) | +| bundle_​errors_​ssl_​alloc | `counter` | Number of gRPC errors encountered (OpenSSL alloc fail) | +| bundle_​heap_​size | `gauge` | Workspace heap size | +| bundle_​heap_​free_​bytes | `gauge` | Approx free space in workspace | +| bundle_​shredstream_​heartbeats | `counter` | Number of ShredStream heartbeats successfully sent | +| bundle_​keepalives | `counter` | Number of HTTP/2 PINGs acknowledged by server | +| bundle_​connected | `gauge` | 1 if connected to the bundle server, 0 if not | ## Verify Tile | Metric | Type | Description | diff --git a/plugin/bundle/test-server/Cargo.toml b/contrib/bundle-test-server/Cargo.toml similarity index 96% rename from plugin/bundle/test-server/Cargo.toml rename to contrib/bundle-test-server/Cargo.toml index b06f00057f..c79849c254 100644 --- a/plugin/bundle/test-server/Cargo.toml +++ b/contrib/bundle-test-server/Cargo.toml @@ -3,9 +3,6 @@ name = "bundle-test-server" version = "0.1.0" edition = "2021" -[[bin]] -name = "bundlesvr" - [dependencies] tonic = { version = "0.12.2", features = ["tls-roots", "tls", "tls-webpki-roots"] } prost = "0.13.3" diff --git a/plugin/bundle/build.rs b/contrib/bundle-test-server/build.rs similarity index 88% rename from plugin/bundle/build.rs rename to contrib/bundle-test-server/build.rs index f316de39e5..e71498c768 100644 --- a/plugin/bundle/build.rs +++ b/contrib/bundle-test-server/build.rs @@ -24,8 +24,8 @@ fn main() -> Result<(), std::io::Error> { } configure() - .build_client(true) - .build_server(false) + .build_client(false) + .build_server(true) .type_attribute( "TransactionErrorType", "#[cfg_attr(test, derive(enum_iterator::Sequence))]", @@ -34,5 +34,5 @@ fn main() -> Result<(), std::io::Error> { "InstructionErrorType", "#[cfg_attr(test, derive(enum_iterator::Sequence))]", ) - .compile_protos(&protos, &[proto_base_path]) + .compile_protos(&protos, &[std::path::PathBuf::from("protos")]) } diff --git a/plugin/bundle/protos/auth.proto b/contrib/bundle-test-server/protos/auth.proto similarity index 100% rename from plugin/bundle/protos/auth.proto rename to contrib/bundle-test-server/protos/auth.proto diff --git a/plugin/bundle/protos/block_engine.proto b/contrib/bundle-test-server/protos/block_engine.proto similarity index 100% rename from plugin/bundle/protos/block_engine.proto rename to contrib/bundle-test-server/protos/block_engine.proto diff --git a/plugin/bundle/protos/bundle.proto b/contrib/bundle-test-server/protos/bundle.proto similarity index 100% rename from plugin/bundle/protos/bundle.proto rename to contrib/bundle-test-server/protos/bundle.proto diff --git a/plugin/bundle/protos/packet.proto b/contrib/bundle-test-server/protos/packet.proto similarity index 100% rename from plugin/bundle/protos/packet.proto rename to contrib/bundle-test-server/protos/packet.proto diff --git a/plugin/bundle/protos/relayer.proto b/contrib/bundle-test-server/protos/relayer.proto similarity index 100% rename from plugin/bundle/protos/relayer.proto rename to contrib/bundle-test-server/protos/relayer.proto diff --git a/plugin/bundle/protos/shared.proto b/contrib/bundle-test-server/protos/shared.proto similarity index 100% rename from plugin/bundle/protos/shared.proto rename to contrib/bundle-test-server/protos/shared.proto diff --git a/plugin/bundle/test-server/src/bin/bundlesvr.rs b/contrib/bundle-test-server/src/main.rs similarity index 87% rename from plugin/bundle/test-server/src/bin/bundlesvr.rs rename to contrib/bundle-test-server/src/main.rs index 987863d3db..73113340c8 100644 --- a/plugin/bundle/test-server/src/bin/bundlesvr.rs +++ b/contrib/bundle-test-server/src/main.rs @@ -1,10 +1,10 @@ use std::iter; use std::pin::Pin; -use bundle_test_server::proto::auth::{self, Token}; -use bundle_test_server::proto::auth::auth_service_server::{AuthService, AuthServiceServer}; -use bundle_test_server::proto::bundle::{Bundle, BundleUuid}; -use bundle_test_server::proto::packet::{Packet, PacketBatch}; +use crate::proto::auth::{self, Token}; +use crate::proto::auth::auth_service_server::{AuthService, AuthServiceServer}; +use crate::proto::bundle::{Bundle, BundleUuid}; +use crate::proto::packet::{Packet, PacketBatch}; use chrono::{Duration, Utc}; use futures::{stream, StreamExt}; use log::info; @@ -13,8 +13,8 @@ use tonic::{transport::Server, Request, Response, Status}; use futures_util::stream::Stream; use base64::prelude::*; -use bundle_test_server::proto::block_engine::block_engine_validator_server::{BlockEngineValidator, BlockEngineValidatorServer}; -use bundle_test_server::proto::block_engine::{SubscribePacketsRequest, SubscribePacketsResponse, SubscribeBundlesRequest, SubscribeBundlesResponse, BlockBuilderFeeInfoRequest, BlockBuilderFeeInfoResponse}; +use crate::proto::block_engine::block_engine_validator_server::{BlockEngineValidator, BlockEngineValidatorServer}; +use crate::proto::block_engine::{SubscribePacketsRequest, SubscribePacketsResponse, SubscribeBundlesRequest, SubscribeBundlesResponse, BlockBuilderFeeInfoRequest, BlockBuilderFeeInfoResponse}; #[derive(Debug, Default)] pub struct BlockEngineValidatorService; @@ -22,6 +22,27 @@ pub struct BlockEngineValidatorService; type PacketResponseStream = Pin> + Send>>; type BundleResponseStream = Pin> + Send>>; +pub mod proto { + pub mod auth { + tonic::include_proto!("auth"); + } + pub mod block_engine { + tonic::include_proto!("block_engine"); + } + pub mod bundle { + tonic::include_proto!("bundle"); + } + pub mod packet { + tonic::include_proto!("packet"); + } + pub mod relayer { + tonic::include_proto!("relayer"); + } + pub mod shared { + tonic::include_proto!("shared"); + } +} + #[tonic::async_trait] impl BlockEngineValidator for BlockEngineValidatorService { type SubscribePacketsStream = PacketResponseStream; @@ -115,14 +136,14 @@ impl AuthService for Auth { ) -> Result, Status> { Ok(Response::new(auth::GenerateAuthTokensResponse { access_token: Some(Token { - value: "".to_string(), + value: "token".to_string(), expires_at_utc: Some(Timestamp { seconds: (Utc::now() + Duration::seconds(60)).timestamp(), nanos: 0, }), }), refresh_token: Some(Token { - value: "".to_string(), + value: "token".to_string(), expires_at_utc: Some(Timestamp { seconds: (Utc::now() + Duration::seconds(60)).timestamp(), nanos: 0, diff --git a/plugin/bundle/Cargo.toml b/plugin/bundle/Cargo.toml deleted file mode 100644 index 57e8361538..0000000000 --- a/plugin/bundle/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "firedancer-plugin-bundle" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["staticlib"] - -[dependencies] -tonic = { version = "0.12.3", features = ["tls-roots", "tls", "tls-webpki-roots"] } -prost = "0.13.3" -prost-types = "0.13.3" -log = "0.4.22" -tokio = "1.40.0" -tokio-stream = "0.1" -futures = "0.3.30" -chrono = "0.4.38" -thiserror = "1.0.64" -bs58 = "0.5.1" - -[build-dependencies] -tonic-build = "0.12.3" -protobuf-src = "2.1.0" -prost-types = "0.13.3" - -[dev-dependencies] -env_logger = "0.11.5" -ed25519-dalek = "2.1.1" - -[profile.release-with-debug] -inherits = "release" -debug = true -split-debuginfo = "packed" diff --git a/plugin/bundle/src/auth.rs b/plugin/bundle/src/auth.rs deleted file mode 100644 index 02be121595..0000000000 --- a/plugin/bundle/src/auth.rs +++ /dev/null @@ -1,196 +0,0 @@ -use { - crate::{ - proto::auth::{ - auth_service_client::AuthServiceClient, GenerateAuthChallengeRequest, - GenerateAuthTokensRequest, RefreshAccessTokenRequest, Role, Token, - }, - ProxyError, - }, - chrono::Utc, - log::*, - std::{ffi::CString, time::Duration}, - tokio::time::timeout, - tokio::sync::watch, - tonic::{service::Interceptor, transport::Channel, Code, Request, Status}, - std::ops::Deref, -}; - -/// Interceptor responsible for adding the access token to request headers. -#[derive(Clone)] -pub(crate) struct AuthInterceptor { - /// The token added to each request header. - access_token: Token, -} - -impl AuthInterceptor { - pub(crate) fn new(access_token: Token) -> Self { - Self { access_token } - } -} - -impl Interceptor for AuthInterceptor { - fn call(&mut self, mut request: Request<()>) -> Result, Status> { - request.metadata_mut().insert( - "authorization", - format!("Bearer {}", self.access_token.value) - .parse() - .map_err(|_| { - Status::invalid_argument("Failed to parse authorization header") - })?, - ); - - Ok(request) - } -} - -extern "C" { - fn plugin_bundle_sign_challenge(challenge: *const i8, result: *mut u8); -} - -/// Generates an auth challenge then generates and returns validated auth tokens. -pub async fn generate_auth_tokens( - auth_service_client: &mut AuthServiceClient, - // used to sign challenges - identity_pubkey: &mut watch::Receiver<[u8; 32]>, -) -> crate::Result<( - Token, /* access_token */ - Token, /* refresh_token */ -)> { - let pubkey = identity_pubkey.borrow_and_update().deref().to_vec(); - - debug!("generate_auth_challenge"); - let challenge_response = auth_service_client - .generate_auth_challenge(GenerateAuthChallengeRequest { - role: Role::Validator as i32, - pubkey: pubkey.clone(), - }) - .await - .map_err(|e: Status| { - if e.code() == Code::PermissionDenied { - ProxyError::AuthenticationPermissionDenied - } else { - ProxyError::AuthenticationError(e.to_string()) - } - })?; - - let challenge = challenge_response.into_inner().challenge; - let challenge_cstr = CString::new(challenge.clone()).map_err(|e| { - ProxyError::AuthenticationError(format!("challenge has internal nul byte {}", e)) - })?; - - if challenge_cstr.as_bytes().len() != 9 { - return Err(ProxyError::AuthenticationError( - "challenge length is not 9".to_string(), - )); - } - - let formatted_challenge = format!( - "{}-{}", - bs58::encode(pubkey.clone()).into_string(), - challenge, - ); - - let mut signed_challenge: [u8; 64] = [0; 64]; - unsafe { - plugin_bundle_sign_challenge(challenge_cstr.as_ptr(), signed_challenge.as_mut_ptr()); - } - - debug!("generate_auth_tokens"); - let auth_tokens = auth_service_client - .generate_auth_tokens(GenerateAuthTokensRequest { - challenge: formatted_challenge, - client_pubkey: pubkey, - signed_challenge: Vec::from(signed_challenge), - }) - .await - .map_err(|e| ProxyError::AuthenticationError(e.to_string()))?; - - let inner = auth_tokens.into_inner(); - let access_token = get_validated_token(inner.access_token)?; - let refresh_token = get_validated_token(inner.refresh_token)?; - - Ok((access_token, refresh_token)) -} - -/// Tries to refresh the access token or run full-reauth if needed. -pub async fn maybe_refresh_auth_tokens( - pubkey: &mut watch::Receiver<[u8; 32]>, - auth_service_client: &mut AuthServiceClient, - access_token: &Token, - refresh_token: &Token, - connection_timeout: &Duration, - refresh_within_s: u64, -) -> crate::Result<( - Option, // access token - Option, // refresh token -)> { - let access_token_expiry: u64 = access_token - .expires_at_utc - .as_ref() - .map(|ts| ts.seconds as u64) - .unwrap_or_default(); - let refresh_token_expiry: u64 = refresh_token - .expires_at_utc - .as_ref() - .map(|ts| ts.seconds as u64) - .unwrap_or_default(); - - let now = Utc::now().timestamp() as u64; - - let should_refresh_access = - access_token_expiry.checked_sub(now).unwrap_or_default() <= refresh_within_s; - let should_generate_new_tokens = pubkey.has_changed().unwrap() || - refresh_token_expiry.checked_sub(now).unwrap_or_default() <= refresh_within_s; - - if should_generate_new_tokens { - let (new_access_token, new_refresh_token) = timeout( - *connection_timeout, - generate_auth_tokens(auth_service_client, pubkey), - ) - .await - .map_err(|_| ProxyError::MethodTimeout("generate_auth_tokens".to_string()))? - .map_err(|e| ProxyError::MethodError(e.to_string()))?; - - return Ok((Some(new_access_token), Some(new_refresh_token))); - } else if should_refresh_access { - let new_access_token = timeout( - *connection_timeout, - refresh_access_token(auth_service_client, refresh_token), - ) - .await - .map_err(|_| ProxyError::MethodTimeout("refresh_access_token".to_string()))? - .map_err(|e| ProxyError::MethodError(e.to_string()))?; - - return Ok((Some(new_access_token), None)); - } - - Ok((None, None)) -} - -pub async fn refresh_access_token( - auth_service_client: &mut AuthServiceClient, - refresh_token: &Token, -) -> crate::Result { - let response = auth_service_client - .refresh_access_token(RefreshAccessTokenRequest { - refresh_token: refresh_token.value.clone(), - }) - .await - .map_err(|e| ProxyError::AuthenticationError(e.to_string()))?; - get_validated_token(response.into_inner().access_token) -} - -/// An invalid token is one where any of its fields are None or the token itself is None. -/// Performs the necessary validations on the auth tokens before returning, -/// i.e. it is safe to call .unwrap() on the token fields from the call-site. -fn get_validated_token(maybe_token: Option) -> crate::Result { - let token = maybe_token - .ok_or_else(|| ProxyError::BadAuthenticationToken("received a null token".to_string()))?; - if token.expires_at_utc.is_none() { - Err(ProxyError::BadAuthenticationToken( - "expires_at_utc field is null".to_string(), - )) - } else { - Ok(token) - } -} diff --git a/plugin/bundle/src/block_engine_stage.rs b/plugin/bundle/src/block_engine_stage.rs deleted file mode 100644 index 6e96309b1e..0000000000 --- a/plugin/bundle/src/block_engine_stage.rs +++ /dev/null @@ -1,660 +0,0 @@ -use { - crate::{ - auth::{generate_auth_tokens, maybe_refresh_auth_tokens, AuthInterceptor}, - proto::{ - auth::{auth_service_client::AuthServiceClient, Token}, - block_engine::{ - self, block_engine_validator_client::BlockEngineValidatorClient, - BlockBuilderFeeInfoRequest, - }, - block_engine::{SubscribeBundlesResponse, SubscribePacketsResponse}, - bundle::BundleUuid, - packet::Packet, - }, - ProxyError, - }, - log::*, - std::ffi::{c_void, CStr}, - std::{ - future::{Future, ready}, - pin::Pin, - sync::{Arc, Mutex}, - task::{Context, Poll}, - time::Duration, - }, - tonic::{ - codegen::InterceptedService, - transport::{Channel, Endpoint}, - Streaming, - }, - futures::{ - StreamExt, - stream::{self, Stream, once}, - stream_select, - }, - tokio::{ - runtime::{self, Runtime}, - time::{interval, sleep, timeout}, - sync::watch, - }, - tokio_stream::wrappers::IntervalStream, -}; - -struct TileFuture { - pending: u64, - stream: Arc + Send>>>>, -} - -impl Future for TileFuture { - type Output = Option; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // Incredibly dumb series of stuff here ... we want to drive the - // future from the tile outer loop, so it's like ... - // - // while (1) { - // do_housekeeping(); - // receive_credits(); - // poll_futures(); - // ... - // } - // - // Unfortunately Tokio makes it extremely difficult to turn the - // futures one time, so this pending count and wake here kind of - // achieve that. I wrote a custom futures executor to do this, - // but unfortunately the libraries require Tokio to use some of - // the I/O functionality and will error without a Tokio Runtime. - cx.waker().clone().wake(); - - if self.pending == 0 { - return Poll::Ready(None); - } - self.pending -= 1; - - let mut stream = self.stream.lock().unwrap(); - match stream.as_mut().poll_next_unpin(cx) { - Poll::Ready(Some(item)) => Poll::Ready(Some(item)), - Poll::Ready(None) => unreachable!(), - Poll::Pending => Poll::Pending, - } - } -} - -struct TileExecutor { - runtime: Runtime, - identity_pubkey: (watch::Sender<[u8; 32]>, watch::Receiver<[u8; 32]>), - stream: Arc + Send>>>>, -} - -#[no_mangle] -pub extern "C" fn plugin_bundle_init( - url: *const i8, - domain_name: *const i8, - pubkey: *const u8, -) -> *mut c_void { - extern "C" { - fn fd_log_private_1( - level: i32, - now: i64, - file: *const i8, - line: i32, - func: *const i8, - msg: *const i8, - ); - fn fd_log_wallclock() -> i64; - fn fd_log_level_logfile() -> i32; - } - - struct FDLogger {} - - impl log::Log for FDLogger { - fn enabled(&self, metadata: &log::Metadata) -> bool { - match metadata.level() { - log::Level::Error | log::Level::Warn | log::Level::Info => true, - log::Level::Debug | log::Level::Trace => false, - } - } - - fn log(&self, record: &log::Record) { - match record.level() { - log::Level::Error | log::Level::Warn | log::Level::Info => (), - log::Level::Debug | log::Level::Trace => return, - }; - - let level: i32 = match record.level() { - log::Level::Error => 4, - log::Level::Warn => 3, - log::Level::Info => 1, /* Info -> DEBUG, so it doesn't spam stdout */ - log::Level::Debug => 1, - log::Level::Trace => 0, - }; - - const UNKNOWN: &'static str = "unknown"; - - let file = if let Some(file) = record.file() { - std::ffi::CString::new(file).unwrap_or(std::ffi::CString::new(UNKNOWN).unwrap()) - } else { - std::ffi::CString::new(UNKNOWN).unwrap() - }; - - let msg = std::ffi::CString::new(record.args().to_string()) - .unwrap_or(std::ffi::CString::new(UNKNOWN).unwrap()); - let target = std::ffi::CString::new(record.target()) - .unwrap_or(std::ffi::CString::new(UNKNOWN).unwrap()); - - unsafe { - // We reroute log messages to the Firedancer logger. - // There are a few problems with this. The message should be - // printed into the target buffer, rather than a heap - // allocated string. None the less, it's good enough for now. - fd_log_private_1( - level, - fd_log_wallclock(), - file.as_ptr(), - record.line().unwrap_or(0) as i32, - target.as_ptr(), - msg.as_ptr(), - ); - } - } - - fn flush(&self) {} - } - let _logger_thread: Option> = None; - static LOGGER: FDLogger = FDLogger {}; - let log_level = match unsafe { fd_log_level_logfile() } { - 0 => LevelFilter::Trace, - 1 => LevelFilter::Debug, - 2 => LevelFilter::Info, - 3 => LevelFilter::Warn, - 4 => LevelFilter::Error, - _ => LevelFilter::Off, - }; - log::set_logger(&LOGGER) - .map(|()| log::set_max_level(log_level)) - .unwrap(); - - let pubkey: [u8; 32] = unsafe { std::ptr::read(pubkey as *const [u8; 32]) }; - let channel = watch::channel(pubkey); - - let task = produce_bundles( - unsafe { CStr::from_ptr(url).to_string_lossy().into_owned() }, - unsafe { CStr::from_ptr(domain_name).to_string_lossy().into_owned() }, - channel.1.clone(), - ); - - let executor = TileExecutor { - runtime: runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build() - .unwrap(), - identity_pubkey: channel, - stream: Arc::new(Mutex::new(Box::pin(task))), - }; - - Box::into_raw(Box::new(executor)) as *mut c_void -} - -#[no_mangle] -pub extern "C" fn plugin_bundle_poll( - plugin: *mut c_void, - reload_identity: i32, - identity_pubkey: *const u8, - out_type: *mut i32, - out_block_builder_pubkey: *mut u8, - out_block_builder_commission: *mut u64, - out_bundle_len: *mut u64, - out_data: *mut u8, -) { - let executor = unsafe { &mut *(plugin as *mut TileExecutor) }; - - if reload_identity != 0 { - let identity: [u8; 32] = unsafe { std::ptr::read(identity_pubkey as *const [u8; 32]) }; - executor.identity_pubkey.0.send(identity).unwrap(); - } - - let future = TileFuture { - pending: 64, - stream: Arc::clone(&executor.stream), - }; - - unsafe { *out_type = 0 }; - - unsafe { *out_bundle_len = 0 }; - match executor.runtime.block_on(future) { - Some(BundleOrPacket::Disconnected) => { - unsafe { *out_type = -1i32 }; - } - Some(BundleOrPacket::Connecting) => { - unsafe { *out_type = -2i32 }; - } - Some(BundleOrPacket::Connected) => { - unsafe { *out_type = -3i32 }; - } - Some(BundleOrPacket::Bundle(bundle, block_builder_pubkey, commission)) => { - let bundle = match bundle.bundle { - None => { - warn!("bundle message has no actual bundle, ignoring"); - return; - } - Some(bundle) => bundle, - }; - - if bundle.packets.is_empty() { - warn!("bundle has no packets, ignoring"); - return; - } - - if bundle.packets.len() > 5 { - warn!("bundle has more than 5 packets, ignoring"); - return; - } - - for packet in &bundle.packets { - if packet.data.len() > 1232 { - warn!("packet data is too large, ignoring"); - return; - } - } - - unsafe { *out_type = 1i32 }; - unsafe { *out_bundle_len = bundle.packets.len() as u64 }; - unsafe { std::ptr::copy(block_builder_pubkey.as_ptr(), out_block_builder_pubkey, 32) }; - unsafe { *out_block_builder_commission = commission }; - - let mut offset: usize = 0; - let out_slice = unsafe { std::slice::from_raw_parts_mut(out_data, 5 * (8 + 1232)) }; - for packet in bundle.packets { - out_slice[offset..offset + 8] - .copy_from_slice(&(packet.data.len() as u64).to_le_bytes()); - out_slice[offset + 8..offset + 8 + packet.data.len()].copy_from_slice(&packet.data); - offset += 8 + packet.data.len(); - } - } - Some(BundleOrPacket::Packet(packet)) => { - if packet.data.len() > 1232 { - warn!("packet data is too large, ignoring"); - return; - } - - unsafe { *out_type = 2i32 }; - unsafe { *out_bundle_len = 1u64 }; - - let out_slice = unsafe { std::slice::from_raw_parts_mut(out_data, 8 + 1232) }; - out_slice[0..8].copy_from_slice(&(packet.data.len() as u64).to_le_bytes()); - out_slice[8..8 + packet.data.len()].copy_from_slice(&packet.data); - } - None => (), - } -} - -enum BundleOrPacket { - Disconnected, - Connecting, - Connected, - Bundle(BundleUuid, [u8; 32], u64), - Packet(Packet), -} - -enum StreamSelector { - Bundle(BundleUuid), - Packet(Packet), - AuthTimer, - MaintenanceTimer, - Err(ProxyError), -} - -struct StreamState { - identity_pubkey: watch::Receiver<[u8; 32]>, - client: BlockEngineValidatorClient>, - auth_client: AuthServiceClient, - access_token: Token, - refresh_token: Token, - block_builder_pubkey: [u8; 32], - block_builder_commission: u64, -} - -fn produce_bundles( - url: String, - domain_name: String, - identity_pubkey: watch::Receiver<[u8; 32]>, -) -> impl Stream { - enum Status { - DisconnectedNotify, - Disconnected, - ConnectingNotify, - Connecting, - ConnectedNotify( - BlockEngineValidatorClient>, - AuthServiceClient, - Token, - Token, - ), - Connected( - BlockEngineValidatorClient>, - AuthServiceClient, - Token, - Token, - ) - } - - let status = Status::Connecting; - stream::unfold((status, url, domain_name, identity_pubkey), |(status, url, domain_name, mut identity_pubkey)| async move { - match status { - Status::DisconnectedNotify => { - Some((Some(once(ready(BundleOrPacket::Disconnected)).boxed()), (Status::Disconnected, url, domain_name, identity_pubkey))) - } - Status::Disconnected => { - sleep(Duration::from_secs(5)).await; - Some((None, (Status::ConnectingNotify, url, domain_name, identity_pubkey))) - } - Status::ConnectingNotify => { - Some((Some(once(ready(BundleOrPacket::Connecting)).boxed()), (Status::Connecting, url, domain_name, identity_pubkey))) - } - Status::ConnectedNotify(client, auth_client, access_token, refresh_token) => { - Some((Some(once(ready(BundleOrPacket::Connected)).boxed()), (Status::Connected(client, auth_client, access_token, refresh_token), url, domain_name, identity_pubkey))) - } - Status::Connecting => { - let (client, auth_client, access_token, refresh_token) = match connect_auth(&url, &domain_name, &mut identity_pubkey).await { - Ok(result) => result, - Err(ProxyError::AuthenticationPermissionDenied) => { - // This error is frequent on hot spares, and the parsed string does not work - // with datapoints (incorrect escaping). - warn!("block engine permission denied. not on leader schedule. ignore if hot-spare."); - return Some((None, (Status::DisconnectedNotify, url, domain_name, identity_pubkey))); - }, - Err(err) => { - warn!("block engine connection error: {:?}", err); - return Some((None, (Status::DisconnectedNotify, url, domain_name, identity_pubkey))); - } - }; - - Some((None, (Status::ConnectedNotify(client, auth_client, access_token, refresh_token), url, domain_name, identity_pubkey))) - } - Status::Connected(mut client, auth_client, access_token, refresh_token) => { - log::info!("Connected to block engine: {}", url); - - let (subscribe_bundles_stream, subscribe_packets_stream, block_builder_pubkey, block_builder_commission) = match subscribe_block_engine_bundles_and_packets(&mut client).await { - Ok(result) => result, - Err(err) => { - warn!("block engine subscription error: {:?}", err); - return Some((None, (Status::DisconnectedNotify, url, domain_name, identity_pubkey))); - } - }; - - let auth_timer = IntervalStream::new(interval(Duration::from_secs(5))).map(|_| StreamSelector::AuthTimer); - let maintenance_timer = IntervalStream::new(interval(Duration::from_secs(600))).map(|_| StreamSelector::MaintenanceTimer); - let combined_stream = stream_select!( - subscribe_bundles_stream.flat_map(|x| match x { - Ok(bundles_response) => futures::stream::iter(bundles_response.bundles.into_iter().map(StreamSelector::Bundle).collect::>()), - Err(_) => futures::stream::iter(vec![StreamSelector::Err(ProxyError::GrpcStreamDisconnected)]), - }), - subscribe_packets_stream.flat_map(|x| match x { - Ok(packets_response) => { - if let Some(batch) = packets_response.batch { - futures::stream::iter(batch.packets.into_iter().map(StreamSelector::Packet).collect::>()).boxed() - } else { - futures::stream::empty().boxed() - } - }, - Err(_) => futures::stream::iter(vec![StreamSelector::Err(ProxyError::GrpcStreamDisconnected)]).boxed(), - }), - auth_timer, - maintenance_timer, - ); - - // Annoying to have to use Arc> here ... technically this shouldn't be - // needed if we could convince the compiler of the right lifetimes. - let state = Arc::new(Mutex::new(StreamState { - identity_pubkey: identity_pubkey.clone(), - client, - auth_client, - access_token, - refresh_token, - block_builder_pubkey, - block_builder_commission, - })); - - let stream = combined_stream.scan(state, |state, item| { - let _state = state.clone(); - - let state = _state.lock().unwrap(); - let mut identity_pubkey = state.identity_pubkey.clone(); - let mut client = state.client.clone(); - let mut auth_client = state.auth_client.clone(); - let access_token = state.access_token.clone(); - let refresh_token = state.refresh_token.clone(); - let block_builder_pubkey = state.block_builder_pubkey.clone(); - let block_builder_commission = state.block_builder_commission.clone(); - drop(state); - - async move { - match item { - StreamSelector::AuthTimer => { - debug!("auth timer"); - let (maybe_new_access, maybe_new_refresh) = match maybe_refresh_auth_tokens( - &mut identity_pubkey, - &mut auth_client, - &access_token, - &refresh_token, - &Duration::from_secs(5), - 1, - ).await { - Ok(result) => result, - Err(err) => return Some((StreamSelector::Err(err), block_builder_pubkey, block_builder_commission)), - }; - - let mut state = _state.lock().unwrap(); - if let Some(new_token) = maybe_new_access { - state.access_token = new_token; - } - if let Some(new_token) = maybe_new_refresh { - state.refresh_token = new_token; - } - } - StreamSelector::MaintenanceTimer => { - debug!("maintenance timer"); - let (block_builder_pubkey, block_builder_commission) = match refresh_block_builder_info(&mut client).await { - Ok(result) => result, - Err(err) => return Some((StreamSelector::Err(err), block_builder_pubkey, block_builder_commission)), - }; - - let mut state = _state.lock().unwrap(); - state.block_builder_pubkey = block_builder_pubkey; - state.block_builder_commission = block_builder_commission; - } - _ => (), - }; - - Some((item, block_builder_pubkey, block_builder_commission)) - } - }); - - let stream = stream.take_while(|item| { - ready(match item { - (StreamSelector::Bundle(_), _, _) => true, - (StreamSelector::Packet(_), _, _) => true, - (StreamSelector::AuthTimer, _, _) => true, - (StreamSelector::MaintenanceTimer, _, _) => true, - (StreamSelector::Err(err), _, _) => { - warn!("stream error: {:?}", err); - false - } - }) - }).filter_map(|item| async { - match item { - (StreamSelector::Bundle(bundle), pubkey, commission) => { - Some(BundleOrPacket::Bundle(bundle, pubkey, commission)) - } - (StreamSelector::Packet(packet), _, _) => { - Some(BundleOrPacket::Packet(packet)) - } - (StreamSelector::AuthTimer, _, _) => None, - (StreamSelector::MaintenanceTimer, _, _) => None, - (StreamSelector::Err(_), _, _) => unreachable!(), - } - }); - - Some((Some(stream.boxed()), (Status::DisconnectedNotify, url, domain_name, identity_pubkey))) - } - } - }).filter_map(|x| async { x }).flatten() -} - -async fn refresh_block_builder_info( - client: &mut BlockEngineValidatorClient>, -) -> crate::Result<([u8; 32], u64)> { - info!("Refreshing block builder info"); - let block_builder_info = timeout( - Duration::from_secs(5), - client.get_block_builder_fee_info(BlockBuilderFeeInfoRequest {}), - ) - .await - .map_err(|_| { - warn!("block engine method timeout: get_block_builder_fee_info"); - ProxyError::MethodTimeout("get_block_builder_fee_info".to_string()) - })? - .map_err(|e| { - warn!("block engine method error: get_block_builder_fee_info: {:?}", e); - ProxyError::MethodError(e.to_string()) - })? - .into_inner(); - info!("Got block builder info"); - - let block_builder_pubkey = bs58::decode(&block_builder_info.pubkey) - .into_vec() - .map_err(|_| { - ProxyError::InvalidData(format!( - "Invalid block_builder pubkey {}", - block_builder_info.pubkey - )) - })? - .try_into() - .map_err(|_| ProxyError::InvalidData("Invalid block_builder pubkey".to_string()))?; - Ok((block_builder_pubkey, block_builder_info.commission)) -} - -async fn connect_auth( - url: &str, - domain_name: &str, - identity_pubkey: &mut watch::Receiver<[u8; 32]>, -) -> crate::Result<( - BlockEngineValidatorClient>, - AuthServiceClient, - Token, - Token, -)> { - let mut backend_endpoint = Endpoint::from_shared(url.to_owned()) - .map_err(|_| { - ProxyError::BlockEngineConnectionError(format!( - "invalid block engine url value: {}", - url - )) - })? - .tcp_keepalive(Some(Duration::from_secs(60))); - - debug!("connecting to block engine: {} ... {}", url, domain_name); - - if url.starts_with("https") { - backend_endpoint = backend_endpoint - .tls_config( - tonic::transport::ClientTlsConfig::new() - .with_webpki_roots() - .domain_name(domain_name), - ) - .map_err(|_| { - ProxyError::BlockEngineConnectionError( - "failed to set tls_config for block engine service".to_string(), - ) - })?; - } - - debug!("connecting to auth: {}", url); - let auth_channel = timeout(Duration::from_secs(5), backend_endpoint.connect()) - .await - .map_err(|_| ProxyError::AuthenticationConnectionTimeout)? - .map_err(|e| ProxyError::AuthenticationConnectionError(format!("{:#?}", e)))?; - - let mut auth_client = AuthServiceClient::new(auth_channel); - - debug!("generating authentication token"); - let (access_token, refresh_token) = timeout( - Duration::from_secs(5), - generate_auth_tokens(&mut auth_client, identity_pubkey), - ) - .await - .map_err(|_| ProxyError::AuthenticationTimeout)??; - - debug!("connecting to block engine: {}", url); - let block_engine_channel = timeout(Duration::from_secs(5), backend_endpoint.connect()) - .await - .map_err(|_| ProxyError::BlockEngineConnectionTimeout)? - .map_err(|e| ProxyError::BlockEngineConnectionError(e.to_string()))?; - - let block_engine_client = BlockEngineValidatorClient::with_interceptor( - block_engine_channel, - AuthInterceptor::new(access_token.clone()), - ); - - Ok(( - block_engine_client, - auth_client, - access_token, - refresh_token, - )) -} - -async fn subscribe_block_engine_bundles_and_packets( - client: &mut BlockEngineValidatorClient>, -) -> crate::Result<( - Streaming, - Streaming, - [u8; 32], - u64, -)> { - let subscribe_packets_stream = timeout( - Duration::from_secs(5), - client.subscribe_packets(block_engine::SubscribePacketsRequest {}), - ) - .await - .map_err(|_| ProxyError::MethodTimeout("block_engine_subscribe_packets".to_string()))? - .map_err(|e| ProxyError::MethodError(e.to_string()))? - .into_inner(); - - let subscribe_bundles_stream = timeout( - Duration::from_secs(5), - client.subscribe_bundles(block_engine::SubscribeBundlesRequest {}), - ) - .await - .map_err(|_| ProxyError::MethodTimeout("subscribe_bundles".to_string()))? - .map_err(|e| ProxyError::MethodError(e.to_string()))? - .into_inner(); - - let block_builder_info = timeout( - Duration::from_secs(5), - client.get_block_builder_fee_info(BlockBuilderFeeInfoRequest {}), - ) - .await - .map_err(|_| ProxyError::MethodTimeout("get_block_builder_fee_info".to_string()))? - .map_err(|e| ProxyError::MethodError(e.to_string()))? - .into_inner(); - - let block_builder_pubkey = bs58::decode(&block_builder_info.pubkey) - .into_vec() - .map_err(|_| { - ProxyError::InvalidData(format!( - "Invalid block_builder pubkey {}", - block_builder_info.pubkey - )) - })? - .try_into() - .map_err(|_| ProxyError::InvalidData("Invalid block_builder pubkey".to_string()))?; - - Ok(( - subscribe_bundles_stream, - subscribe_packets_stream, - block_builder_pubkey, - block_builder_info.commission, - )) -} diff --git a/plugin/bundle/src/lib.rs b/plugin/bundle/src/lib.rs deleted file mode 100644 index cceb939fae..0000000000 --- a/plugin/bundle/src/lib.rs +++ /dev/null @@ -1,85 +0,0 @@ -mod auth; -mod proto; -pub mod block_engine_stage; - -use { - std::{net::AddrParseError, result}, - thiserror::Error, - tonic::Status, -}; - -type Result = result::Result; - -pub use block_engine_stage::{plugin_bundle_init, plugin_bundle_poll}; - -#[derive(Error, Debug)] -pub enum ProxyError { - #[error("grpc error: {0}")] - GrpcError(#[from] Status), - - #[error("stream disconnected")] - GrpcStreamDisconnected, - - #[error("heartbeat error")] - HeartbeatChannelError, - - #[error("heartbeat expired")] - HeartbeatExpired, - - #[error("error forwarding packet to banking stage")] - PacketForwardError, - - #[error("missing tpu config: {0:?}")] - MissingTpuSocket(String), - - #[error("invalid socket address: {0:?}")] - InvalidSocketAddress(#[from] AddrParseError), - - #[error("invalid gRPC data: {0:?}")] - InvalidData(String), - - #[error("timeout: {0:?}")] - ConnectionError(#[from] tonic::transport::Error), - - #[error("AuthenticationConnectionTimeout")] - AuthenticationConnectionTimeout, - - #[error("AuthenticationTimeout")] - AuthenticationTimeout, - - #[error("AuthenticationConnectionError: {0:?}")] - AuthenticationConnectionError(String), - - #[error("BlockEngineConnectionTimeout")] - BlockEngineConnectionTimeout, - - #[error("BlockEngineTimeout")] - BlockEngineTimeout, - - #[error("BlockEngineConnectionError: {0:?}")] - BlockEngineConnectionError(String), - - #[error("RelayerConnectionTimeout")] - RelayerConnectionTimeout, - - #[error("RelayerTimeout")] - RelayerEngineTimeout, - - #[error("RelayerConnectionError: {0:?}")] - RelayerConnectionError(String), - - #[error("AuthenticationError: {0:?}")] - AuthenticationError(String), - - #[error("AuthenticationPermissionDenied")] - AuthenticationPermissionDenied, - - #[error("BadAuthenticationToken: {0:?}")] - BadAuthenticationToken(String), - - #[error("MethodTimeout: {0:?}")] - MethodTimeout(String), - - #[error("MethodError: {0:?}")] - MethodError(String), -} diff --git a/plugin/bundle/src/proto.rs b/plugin/bundle/src/proto.rs deleted file mode 100644 index beb4017914..0000000000 --- a/plugin/bundle/src/proto.rs +++ /dev/null @@ -1,18 +0,0 @@ -pub mod auth { - tonic::include_proto!("auth"); -} -pub mod block_engine { - tonic::include_proto!("block_engine"); -} -pub mod bundle { - tonic::include_proto!("bundle"); -} -pub mod packet { - tonic::include_proto!("packet"); -} -pub mod relayer { - tonic::include_proto!("relayer"); -} -pub mod shared { - tonic::include_proto!("shared"); -} diff --git a/plugin/bundle/test-server/build.rs b/plugin/bundle/test-server/build.rs deleted file mode 100644 index 038f131644..0000000000 --- a/plugin/bundle/test-server/build.rs +++ /dev/null @@ -1,38 +0,0 @@ -use tonic_build::configure; - -fn main() -> Result<(), std::io::Error> { - const PROTOC_ENVAR: &str = "PROTOC"; - if std::env::var(PROTOC_ENVAR).is_err() { - #[cfg(not(windows))] - std::env::set_var(PROTOC_ENVAR, protobuf_src::protoc()); - } - - let proto_base_path = std::path::PathBuf::from("../protos"); - let proto_files = [ - "auth.proto", - "block_engine.proto", - "bundle.proto", - "packet.proto", - "relayer.proto", - "shared.proto", - ]; - let mut protos = Vec::new(); - for proto_file in &proto_files { - let proto = proto_base_path.join(proto_file); - println!("cargo:rerun-if-changed={}", proto.display()); - protos.push(proto); - } - - configure() - .build_client(false) - .build_server(true) - .type_attribute( - "TransactionErrorType", - "#[cfg_attr(test, derive(enum_iterator::Sequence))]", - ) - .type_attribute( - "InstructionErrorType", - "#[cfg_attr(test, derive(enum_iterator::Sequence))]", - ) - .compile_protos(&protos, &[std::path::PathBuf::from("../protos")]) -} diff --git a/plugin/bundle/test-server/src/lib.rs b/plugin/bundle/test-server/src/lib.rs deleted file mode 100644 index 5de4e45879..0000000000 --- a/plugin/bundle/test-server/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub mod proto { - pub mod auth { - tonic::include_proto!("auth"); - } - pub mod block_engine { - tonic::include_proto!("block_engine"); - } - pub mod bundle { - tonic::include_proto!("bundle"); - } - pub mod packet { - tonic::include_proto!("packet"); - } - pub mod relayer { - tonic::include_proto!("relayer"); - } - pub mod shared { - tonic::include_proto!("shared"); - } -} diff --git a/plugin/bundle/tests/testnet.rs b/plugin/bundle/tests/testnet.rs deleted file mode 100644 index 6068f9688c..0000000000 --- a/plugin/bundle/tests/testnet.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::ffi::CString; - -use ed25519_dalek::{ed25519::signature::SignerMut, SigningKey}; -use firedancer_plugin_bundle::*; - -const PRIVATE_KEY: [u8; 32] = [0; 32]; -const PUBLIC_KEY: [u8; 32] = [0; 32]; - -#[no_mangle] -extern "C" fn plugin_bundle_sign_challenge(challenge: *const i8, result: *mut u8) { - let challenge = unsafe { std::ffi::CStr::from_ptr(challenge) } - .to_str() - .unwrap(); - let result = unsafe { std::slice::from_raw_parts_mut(result, 64) }; - - let mut signing_key: SigningKey = SigningKey::from_bytes(&PRIVATE_KEY); - let signature = signing_key - .sign(format!("{}-{}", bs58::encode(PUBLIC_KEY).into_string(), challenge).as_bytes()); - - result.copy_from_slice(&signature.to_bytes()); -} - -#[test] -fn testnet() { - env_logger::init_from_env(env_logger::Env::default().default_filter_or("trace")); - - let url = CString::new(env!("FD_BLOCK_ENGINE_URL")).unwrap(); - - let bundle = plugin_bundle_init(url.as_ptr(), PUBLIC_KEY.as_ptr()); - assert!(!bundle.is_null()); - - loop { - let mut out_bundle_len: u64 = 0; - let mut data: [u8; 6200] = [0; 6200]; - - let mut block_builder_pubkey: [u8; 32] = [0; 32]; - let mut block_builder_commission: u64 = 0; - - plugin_bundle_poll( - bundle, - 0, - std::ptr::null_mut(), - block_builder_pubkey.as_mut_ptr(), - &mut block_builder_commission as &mut u64, - &mut out_bundle_len as *mut u64, - data.as_mut_ptr(), - ); - if out_bundle_len != 0 { - log::warn!( - "Got bundle of len: {}, commission {}, block builder {}", - out_bundle_len, - block_builder_commission, - bs58::encode(block_builder_pubkey).into_string() - ); - } - } -} diff --git a/src/app/fdctl/Local.mk b/src/app/fdctl/Local.mk index e3a3f7a158..4f28b93bee 100644 --- a/src/app/fdctl/Local.mk +++ b/src/app/fdctl/Local.mk @@ -30,7 +30,7 @@ endif $(OBJDIR)/obj/app/fdctl/version.d: src/app/fdctl/version.h -.PHONY: fdctl cargo-validator cargo-solana cargo-ledger-tool cargo-plugin-bundle rust solana check-agave-hash +.PHONY: fdctl cargo-validator cargo-solana cargo-ledger-tool rust solana check-agave-hash # fdctl comands $(call add-objs,commands/run_agave,fd_fdctl) @@ -39,7 +39,7 @@ $(call add-objs,commands/run_agave,fd_fdctl) $(call make-lib,fdctl_version) $(call add-objs,version,fdctl_version) -$(call make-bin-rust,fdctl,main,fd_fdctl fdctl_shared fd_discoh fd_disco agave_validator firedancer_plugin_bundle fd_flamenco fd_quic fd_tls fd_reedsol fd_waltz fd_tango fd_ballet fd_util fdctl_version) +$(call make-bin-rust,fdctl,main,fd_fdctl fdctl_shared fd_discoh fd_disco agave_validator fd_flamenco fd_quic fd_tls fd_reedsol fd_waltz fd_tango fd_ballet fd_util fdctl_version) check-agave-hash: @$(eval AGAVE_COMMIT_LS_TREE=$(shell git ls-tree HEAD | grep agave | awk '{print $$3}')) @@ -54,7 +54,6 @@ check-agave-hash: cargo-validator: check-agave-hash cargo-solana: check-agave-hash cargo-ledger-tool: check-agave-hash -cargo-plugin-bundle: check-agave-hash # Cargo build cannot cache the prior build if the command line changes, # for example if we did, @@ -68,8 +67,6 @@ cargo-plugin-bundle: check-agave-hash # with one cargo command, even if the dependency could be more fine # grained. ifeq ($(RUST_PROFILE),release) -cargo-plugin-bundle: - cd ./plugin/bundle && env --unset=LDFLAGS RUSTFLAGS="$(RUSTFLAGS)" ./cargo build --release --lib -p firedancer-plugin-bundle cargo-validator: cd ./agave && env --unset=LDFLAGS RUSTFLAGS="$(RUSTFLAGS)" ./cargo build --release --lib -p agave-validator cargo-solana: $(OBJDIR)/lib/libfdctl_version.a @@ -77,8 +74,6 @@ cargo-solana: $(OBJDIR)/lib/libfdctl_version.a cargo-ledger-tool: $(OBJDIR)/lib/libfdctl_version.a cd ./agave && env --unset=LDFLAGS RUSTFLAGS="$(RUSTFLAGS) -L $(realpath $(OBJDIR)/lib) -l fdctl_version" ./cargo build --release --bin agave-ledger-tool else ifeq ($(RUST_PROFILE),release-with-debug) -cargo-plugin-bundle: - cd ./plugin/bundle && env --unset=LDFLAGS RUSTFLAGS="$(RUSTFLAGS)" ../../agave/cargo build --profile=release-with-debug --lib -p firedancer-plugin-bundle cargo-validator: cd ./agave && env --unset=LDFLAGS RUSTFLAGS="$(RUSTFLAGS)" ./cargo build --profile=release-with-debug --lib -p agave-validator cargo-solana: $(OBJDIR)/lib/libfdctl_version.a @@ -86,8 +81,6 @@ cargo-solana: $(OBJDIR)/lib/libfdctl_version.a cargo-ledger-tool: $(OBJDIR)/lib/libfdctl_version.a cd ./agave && env --unset=LDFLAGS RUSTFLAGS="$(RUSTFLAGS) -L $(realpath $(OBJDIR)/lib) -l fdctl_version" ./cargo build --profile=release-with-debug --bin agave-ledger-tool else -cargo-plugin-bundle: - cd ./plugin/bundle && env --unset=LDFLAGS RUSTFLAGS="$(RUSTFLAGS)" ./cargo build --lib -p firedancer-plugin-bundle cargo-validator: cd ./agave && env --unset=LDFLAGS RUSTFLAGS="$(RUSTFLAGS)" ./cargo build --lib -p agave-validator cargo-solana: $(OBJDIR)/lib/libfdctl_version.a @@ -104,9 +97,6 @@ endif agave/target/$(RUST_PROFILE)/libagave_validator.a: cargo-validator @sleep 0.1 -plugin/bundle/target/$(RUST_PROFILE)/libfiredancer_plugin_bundle.a: cargo-plugin-bundle - @sleep 0.1 - agave/target/$(RUST_PROFILE)/solana: cargo-solana agave/target/$(RUST_PROFILE)/agave-ledger-tool: cargo-ledger-tool @@ -114,9 +104,6 @@ agave/target/$(RUST_PROFILE)/agave-ledger-tool: cargo-ledger-tool $(OBJDIR)/lib/libagave_validator.a: agave/target/$(RUST_PROFILE)/libagave_validator.a $(MKDIR) $(dir $@) && cp agave/target/$(RUST_PROFILE)/libagave_validator.a $@ -$(OBJDIR)/lib/libfiredancer_plugin_bundle.a: plugin/bundle/target/$(RUST_PROFILE)/libfiredancer_plugin_bundle.a - $(MKDIR) $(dir $@) && cp plugin/bundle/target/$(RUST_PROFILE)/libfiredancer_plugin_bundle.a $@ - fdctl: $(OBJDIR)/bin/fdctl $(OBJDIR)/bin/solana: agave/target/$(RUST_PROFILE)/solana diff --git a/src/app/fdctl/config/default.toml b/src/app/fdctl/config/default.toml index 09b6ab1948..374468ada3 100644 --- a/src/app/fdctl/config/default.toml +++ b/src/app/fdctl/config/default.toml @@ -1154,6 +1154,20 @@ dynamic_port_range = "8900-9000" # portion. commission_bps = 0 + # The validator keeps the gRPC connection to the bundle server + # alive indefinitely, even outside of leader slots. This is + # done using HTTP/2 PING frames. This option roughly configures + # the time between keepalives. The actual timing is randomized + # to reduce burst loads on the network. Must be within range + # [3,3600]. + keepalive_interval_seconds = 5.0 + + # Consider a connection failed if the bundle server fails to + # respond to a request in the given time frame. The validator + # will reconnect eventually, but with increasing delays if there + # are repeated failures. Must be within range [0.5,60]. + request_timeout_seconds = 3.0 + # The pack tile takes incoming transactions that have been verified # by the verify tile and then deduplicated, and attempts to order # them in an optimal way to generate the most fees per compute @@ -1588,6 +1602,32 @@ dynamic_port_range = "8900-9000" # with this option enabled will always be leader. disable_status_cache = false + # Experimental options for the bundle tile. These should not be + # changed on a production validator. + [development.bundle] + # Specify the maximum Protobuf message size in KiB. + # + # This affects the size of various gRPC-related buffers, + # including socket buffer sizes, the TLS buffer, the HTTP/2 + # frame buffer, and a Protobuf encode buffer. + buffer_size_kib = 1024 + + # SSL library heap size in MiB. + # + # When connecting to an HTTPS bundle endpoint, the SSL library + # dynamic allocations in a fixed size heap region. This option + # controls the size of that region. + ssl_heap_size_mib = 64 + + # Log TLS encryption keys for block engine connection. An empty + # string (the default) disables key logging. + # + # Keys are appended in NSS SSLKEYLOGFILE format to the given + # file path. A file is created if none exists. The resulting + # file can be used to decrypt packet captures of gRPC conns + # made by the bundle tile. + ssl_key_log_file = "" + # The following options relate to the 'fddev pktgen' tool only. # pktgen tests net tile transmit throughput by generating a flood of # non-routable Ethernet frames. Only use 'fddev pktgen' if you know diff --git a/src/app/fdctl/config/testnet-jito.toml b/src/app/fdctl/config/testnet-jito.toml index a014bb0f04..70966cc01d 100644 --- a/src/app/fdctl/config/testnet-jito.toml +++ b/src/app/fdctl/config/testnet-jito.toml @@ -38,7 +38,10 @@ scratch_directory = "/data/fd1" [tiles.bundle] enabled = true - url = "https://ny.testnet.block-engine.jito.wtf" + url = "https://testnet.block-engine.jito.wtf" + tip_distribution_program_addr = "F2Zu7QZiTYUhPd7u9ukRVwxh7B71oA3NMJcHuCHc29P2" + tip_payment_program_addr = "GJHtFqM9agxPmkeKjHny6qiRKrXZALvvFGiKf11QE7hy" + tip_distribution_authority = "B1mrQSpdeMU9gCvkJ6VsXVVoYjRGkNA7TtjMyqxrhecH" [tiles.gui] gui_listen_address = "0.0.0.0" diff --git a/src/app/fdctl/topology.c b/src/app/fdctl/topology.c index d42f39987f..50acfecc7c 100644 --- a/src/app/fdctl/topology.c +++ b/src/app/fdctl/topology.c @@ -379,9 +379,15 @@ fd_topo_initialize( config_t * config ) { } else if( FD_UNLIKELY( !strcmp( tile->name, "bundle" ) ) ) { strncpy( tile->bundle.url, config->tiles.bundle.url, sizeof(tile->bundle.url) ); - strncpy( tile->bundle.tls_domain_name, config->tiles.bundle.tls_domain_name, sizeof(tile->bundle.tls_domain_name) ); + tile->bundle.url_len = strnlen( tile->bundle.url, 255 ); + strncpy( tile->bundle.sni, config->tiles.bundle.tls_domain_name, 256 ); + tile->bundle.sni_len = strnlen( tile->bundle.sni, 255 ); strncpy( tile->bundle.identity_key_path, config->paths.identity_key, sizeof(tile->bundle.identity_key_path) ); - + strncpy( tile->bundle.key_log_path, config->development.bundle.ssl_key_log_file, sizeof(tile->bundle.key_log_path) ); + tile->bundle.buf_sz = config->development.bundle.buffer_size_kib<<10; + tile->bundle.ssl_heap_sz = config->development.bundle.ssl_heap_size_mib<<20; + tile->bundle.keepalive_interval_nanos = (ulong)( config->tiles.bundle.keepalive_interval_seconds * 1e9f ); + tile->bundle.request_timeout_nanos = (ulong)( config->tiles.bundle.request_timeout_seconds * 1e9f ); } else if( FD_UNLIKELY( !strcmp( tile->name, "verify" ) ) ) { tile->verify.tcache_depth = config->tiles.verify.signature_cache_size; diff --git a/src/app/fddev/Local.mk b/src/app/fddev/Local.mk index 7b133b0a2d..4cb54d956d 100644 --- a/src/app/fddev/Local.mk +++ b/src/app/fddev/Local.mk @@ -12,7 +12,7 @@ $(call add-objs,commands/configure/blockstore,fd_fddev) $(call add-objs,commands/bench,fd_fddev) $(call add-objs,commands/dev,fd_fddev) -$(call make-bin-rust,fddev,main,fd_fddev fd_fdctl fddev_shared fdctl_shared fd_discoh fd_disco agave_validator firedancer_plugin_bundle fd_flamenco fd_quic fd_tls fd_reedsol fd_waltz fd_tango fd_ballet fd_util fdctl_version) +$(call make-bin-rust,fddev,main,fd_fddev fd_fdctl fddev_shared fdctl_shared fd_discoh fd_disco agave_validator fd_flamenco fd_quic fd_tls fd_reedsol fd_waltz fd_tango fd_ballet fd_util fdctl_version) ifeq (run,$(firstword $(MAKECMDGOALS))) RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) @@ -38,7 +38,7 @@ endif monitor: bin $(OBJDIR)/bin/fddev monitor $(MONITOR_ARGS) -$(call make-integration-test,test_fddev,tests/test_fddev,fd_fddev fd_fdctl fddev_shared fdctl_shared fd_discoh fd_disco agave_validator firedancer_plugin_bundle fd_flamenco fd_quic fd_tls fd_reedsol fd_waltz fd_tango fd_ballet fd_util fdctl_version) +$(call make-integration-test,test_fddev,tests/test_fddev,fd_fddev fd_fdctl fddev_shared fdctl_shared fd_discoh fd_disco agave_validator fd_flamenco fd_quic fd_tls fd_reedsol fd_waltz fd_tango fd_ballet fd_util fdctl_version) $(call run-integration-test,test_fddev) endif diff --git a/src/app/fddev/main.h b/src/app/fddev/main.h index e5847c966d..9f2480f1db 100644 --- a/src/app/fddev/main.h +++ b/src/app/fddev/main.h @@ -118,6 +118,7 @@ extern action_t fd_action_netconf; extern action_t fd_action_set_identity; extern action_t fd_action_version; extern action_t fd_action_bench; +extern action_t fd_action_bundle_client; extern action_t fd_action_dev; extern action_t fd_action_dev1; extern action_t fd_action_dump; @@ -143,6 +144,7 @@ action_t * ACTIONS[] = { &fd_action_help, &fd_action_version, &fd_action_bench, + &fd_action_bundle_client, &fd_action_dev, &fd_action_dev1, &fd_action_dump, diff --git a/src/app/firedancer-dev/Local.mk b/src/app/firedancer-dev/Local.mk index bc98d0edc2..4105af0b65 100644 --- a/src/app/firedancer-dev/Local.mk +++ b/src/app/firedancer-dev/Local.mk @@ -15,7 +15,7 @@ $(call add-objs,commands/dev,fd_firedancer_dev) $(call add-objs,commands/sim,fd_firedancer_dev) $(call add-objs,commands/backtest,fd_firedancer_dev) -$(call make-bin,firedancer-dev,main,fd_firedancer_dev fd_firedancer fddev_shared fdctl_shared fd_discof fd_disco fd_choreo fd_flamenco fd_funk fd_quic fd_tls fd_reedsol fd_waltz fd_tango fd_ballet fd_util firedancer_version, $(SECP256K1_LIBS) $(ROCKSDB_LIBS)) +$(call make-bin,firedancer-dev,main,fd_firedancer_dev fd_firedancer fddev_shared fdctl_shared fd_discof fd_disco fd_choreo fd_flamenco fd_funk fd_quic fd_tls fd_reedsol fd_waltz fd_tango fd_ballet fd_util firedancer_version,$(SECP256K1_LIBS) $(ROCKSDB_LIBS) -lssl -lcrypto) firedancer-dev: $(OBJDIR)/bin/firedancer-dev diff --git a/src/app/firedancer-dev/main.c b/src/app/firedancer-dev/main.c index 416df853bd..feb51498e3 100644 --- a/src/app/firedancer-dev/main.c +++ b/src/app/firedancer-dev/main.c @@ -79,6 +79,7 @@ extern fd_topo_run_tile_t fd_tile_plugin; extern fd_topo_run_tile_t fd_tile_bencho; extern fd_topo_run_tile_t fd_tile_benchg; extern fd_topo_run_tile_t fd_tile_benchs; +extern fd_topo_run_tile_t fd_tile_bundle; extern fd_topo_run_tile_t fd_tile_gossip; extern fd_topo_run_tile_t fd_tile_repair; @@ -119,6 +120,7 @@ fd_topo_run_tile_t * TILES[] = { &fd_tile_bencho, &fd_tile_benchg, &fd_tile_benchs, + &fd_tile_bundle, &fd_tile_gossip, &fd_tile_repair, &fd_tile_storei, @@ -154,6 +156,7 @@ extern action_t fd_action_netconf; extern action_t fd_action_set_identity; extern action_t fd_action_version; extern action_t fd_action_bench; +extern action_t fd_action_bundle_client; extern action_t fd_action_dev; extern action_t fd_action_dump; extern action_t fd_action_flame; @@ -180,6 +183,7 @@ action_t * ACTIONS[] = { &fd_action_help, &fd_action_version, &fd_action_bench, + &fd_action_bundle_client, &fd_action_dev, &fd_action_dump, &fd_action_flame, diff --git a/src/app/firedancer/Local.mk b/src/app/firedancer/Local.mk index 90808731da..11488ba97b 100644 --- a/src/app/firedancer/Local.mk +++ b/src/app/firedancer/Local.mk @@ -37,7 +37,7 @@ $(call add-objs,callbacks,fd_firedancer) $(call make-lib,firedancer_version) $(call add-objs,version,firedancer_version) -$(call make-bin,firedancer,main,fd_firedancer fdctl_shared fd_discof fd_disco fd_choreo fd_flamenco fd_funk fd_quic fd_tls fd_reedsol fd_waltz fd_tango fd_ballet fd_util firedancer_version, $(SECP256K1_LIBS)) +$(call make-bin,firedancer,main,fd_firedancer fdctl_shared fd_discof fd_disco fd_choreo fd_flamenco fd_funk fd_quic fd_tls fd_reedsol fd_waltz fd_tango fd_ballet fd_util firedancer_version,$(SECP256K1_LIBS) -lssl -lcrypto) firedancer: $(OBJDIR)/bin/firedancer else diff --git a/src/app/firedancer/config/default.toml b/src/app/firedancer/config/default.toml index 5380b39db2..96c8687fa6 100644 --- a/src/app/firedancer/config/default.toml +++ b/src/app/firedancer/config/default.toml @@ -859,6 +859,20 @@ user = "" # portion. commission_bps = 0 + # The validator keeps the gRPC connection to the bundle server + # alive indefinitely, even outside of leader slots. This is + # done using HTTP/2 PING frames. This option roughly configures + # the time between keepalives. The actual timing is randomized + # to reduce burst loads on the network. Must be within range + # [3,3600]. + keepalive_interval_seconds = 5.0 + + # Consider a connection failed if the bundle server fails to + # respond to a request in the given time frame. The validator + # will reconnect eventually, but with increasing delays if there + # are repeated failures. Must be within range [0.5,60]. + request_timeout_seconds = 3.0 + # The pack tile takes incoming transactions that have been verified # by the verify tile and then deduplicated, and attempts to order # them in an optimal way to generate the most fees per compute @@ -1304,6 +1318,32 @@ user = "" # the rest of the network. larger_shred_limits_per_block = false + # Experimental options for the bundle tile. These should not be + # changed on a production validator. + [development.bundle] + # Specify the maximum Protobuf message size in KiB. + # + # This affects the size of various gRPC-related buffers, + # including socket buffer sizes, the TLS buffer, the HTTP/2 + # frame buffer, and a Protobuf encode buffer. + buffer_size_kib = 1024 + + # SSL library heap size in MiB. + # + # When connecting to an HTTPS bundle endpoint, the SSL library + # dynamic allocations in a fixed size heap region. This option + # controls the size of that region. + ssl_heap_size_mib = 64 + + # Log TLS encryption keys for block engine connection. An empty + # string (the default) disables key logging. + # + # Keys are appended in NSS SSLKEYLOGFILE format to the given + # file path. A file is created if none exists. The resulting + # file can be used to decrypt packet captures of gRPC conns + # made by the bundle tile. + ssl_key_log_file = "" + # The following options relate to the 'firedance-dev pktgen' tool # only. pktgen tests net tile transmit throughput by generating a # flood of non-routable Ethernet frames. Only use 'firedancer-dev diff --git a/src/app/firedancer/main.c b/src/app/firedancer/main.c index 3ef95ce793..5fb2d8d22e 100644 --- a/src/app/firedancer/main.c +++ b/src/app/firedancer/main.c @@ -66,6 +66,7 @@ extern fd_topo_run_tile_t fd_tile_metric; extern fd_topo_run_tile_t fd_tile_cswtch; extern fd_topo_run_tile_t fd_tile_gui; extern fd_topo_run_tile_t fd_tile_plugin; +extern fd_topo_run_tile_t fd_tile_bundle; extern fd_topo_run_tile_t fd_tile_gossip; extern fd_topo_run_tile_t fd_tile_repair; @@ -95,6 +96,7 @@ fd_topo_run_tile_t * TILES[] = { &fd_tile_cswtch, &fd_tile_gui, &fd_tile_plugin, + &fd_tile_bundle, &fd_tile_gossip, &fd_tile_repair, &fd_tile_storei, diff --git a/src/app/shared/commands/help.c b/src/app/shared/commands/help.c index 3d38cc3199..bfd0ca3970 100644 --- a/src/app/shared/commands/help.c +++ b/src/app/shared/commands/help.c @@ -21,7 +21,7 @@ help_cmd_fn( args_t * args FD_PARAM_UNUSED, FD_LOG_STDOUT(( " --help Print this help message\n\n" )); FD_LOG_STDOUT(( "SUBCOMMANDS:\n" )); for( ulong i=0UL; ACTIONS[ i ]; i++ ) { - FD_LOG_STDOUT(( " %12s %s\n", ACTIONS[ i ]->name, ACTIONS[ i ]->description )); + FD_LOG_STDOUT(( " %13s %s\n", ACTIONS[ i ]->name, ACTIONS[ i ]->description )); } } diff --git a/src/app/shared/fd_config.c b/src/app/shared/fd_config.c index 9cf437e714..befaff1ed6 100644 --- a/src/app/shared/fd_config.c +++ b/src/app/shared/fd_config.c @@ -431,6 +431,19 @@ fd_config_fill( fd_config_t * config, } \ } while(0) +FD_FN_CONST static int +fd_float_in_range( float value, + float low, + float high ) { + return value>=low && value<=high; +} + +#define CFG_FLOAT_RANGE_CHECK( value, low, high ) do { \ + if( FD_UNLIKELY( !fd_float_in_range( (config->value), (low), (high) ) ) ) { \ + FD_LOG_ERR(( "`%s` must be in range [%f,%f]", #value, (double)(low), (double)(high) )); \ + } \ +} while(0) + static void fd_config_validatef( fd_configf_t const * config ) { (void)config; @@ -523,6 +536,9 @@ fd_config_validate( fd_config_t const * config ) { CFG_HAS_NON_ZERO( tiles.gui.gui_listen_port ); + CFG_FLOAT_RANGE_CHECK( tiles.bundle.keepalive_interval_seconds, 3.0f, 3600.f ); + CFG_FLOAT_RANGE_CHECK( tiles.bundle.request_timeout_seconds, 0.5f, 60.f ); + CFG_HAS_NON_EMPTY( development.netns.interface0 ); CFG_HAS_NON_EMPTY( development.netns.interface0_mac ); CFG_HAS_NON_EMPTY( development.netns.interface0_addr ); @@ -538,6 +554,8 @@ fd_config_validate( fd_config_t const * config ) { CFG_HAS_NON_ZERO ( development.bench.benchg_tile_count ); CFG_HAS_NON_ZERO ( development.bench.benchs_tile_count ); CFG_HAS_NON_EMPTY( development.bench.affinity ); + + CFG_HAS_NON_ZERO( development.bundle.ssl_heap_size_mib ); } #undef CFG_HAS_NON_EMPTY diff --git a/src/app/shared/fd_config.h b/src/app/shared/fd_config.h index a658cebc95..bb46adab2b 100644 --- a/src/app/shared/fd_config.h +++ b/src/app/shared/fd_config.h @@ -283,6 +283,12 @@ struct fd_config { int disable_status_cache; } bench; + struct { + char ssl_key_log_file[ PATH_MAX ]; + uint buffer_size_kib; + uint ssl_heap_size_mib; + } bundle; + struct { char affinity[ AFFINITY_SZ ]; char fake_dst_ip[ 16 ]; @@ -325,6 +331,8 @@ struct fd_config { char tip_payment_program_addr[ FD_BASE58_ENCODED_32_SZ ]; char tip_distribution_authority[ FD_BASE58_ENCODED_32_SZ ]; uint commission_bps; + float keepalive_interval_seconds; + float request_timeout_seconds; } bundle; struct { diff --git a/src/app/shared/fd_config_parse.c b/src/app/shared/fd_config_parse.c index 9737796419..21bfc3486e 100644 --- a/src/app/shared/fd_config_parse.c +++ b/src/app/shared/fd_config_parse.c @@ -54,6 +54,40 @@ fdctl_cfg_get_ulong( ulong * out, return 1; } +static int +fdctl_cfg_get_float( float * out, + ulong out_sz FD_PARAM_UNUSED, + fd_pod_info_t const * info, + char const * path ) { + + ulong unum; + float num; + switch( info->val_type ) { + case FD_POD_VAL_TYPE_LONG: + fd_ulong_svw_dec( (uchar const *)info->val, &unum ); + long snum = fd_long_zz_dec( unum ); + if( snum < 0L ) { + FD_LOG_WARNING(( "`%s` cannot be negative", path )); + return 0; + } + num = (float)snum; + break; + case FD_POD_VAL_TYPE_ULONG: + fd_ulong_svw_dec( (uchar const *)info->val, &unum ); + num = (float)unum; + break; + case FD_POD_VAL_TYPE_FLOAT: + num = FD_LOAD( float, info->val ); + break; + default: + FD_LOG_WARNING(( "invalid value for `%s`", path )); + return 0; + } + + *out = num; + return 1; +} + static int fdctl_cfg_get_uint( uint * out, ulong out_sz FD_PARAM_UNUSED, @@ -416,6 +450,8 @@ fd_config_extract_pod( uchar * pod, CFG_POP ( cstr, tiles.bundle.tip_payment_program_addr ); CFG_POP ( cstr, tiles.bundle.tip_distribution_authority ); CFG_POP ( uint, tiles.bundle.commission_bps ); + CFG_POP ( float, tiles.bundle.keepalive_interval_seconds ); + CFG_POP ( float, tiles.bundle.request_timeout_seconds ); CFG_POP ( uint, tiles.pack.max_pending_transactions ); CFG_POP ( bool, tiles.pack.use_consumed_cus ); @@ -508,6 +544,10 @@ fd_config_extract_pod( uchar * pod, CFG_POP ( ulong, development.bench.disable_blockstore_from_slot ); CFG_POP ( bool, development.bench.disable_status_cache ); + CFG_POP ( cstr, development.bundle.ssl_key_log_file ); + CFG_POP ( uint, development.bundle.buffer_size_kib ); + CFG_POP ( uint, development.bundle.ssl_heap_size_mib ); + CFG_POP ( cstr, development.pktgen.affinity ); CFG_POP ( cstr, development.pktgen.fake_dst_ip ); diff --git a/src/app/shared_dev/Local.mk b/src/app/shared_dev/Local.mk index 64ac753a96..49d35b397d 100644 --- a/src/app/shared_dev/Local.mk +++ b/src/app/shared_dev/Local.mk @@ -9,6 +9,7 @@ $(call add-objs,boot/fd_dev_boot,fddev_shared) # fddev actions $(call add-objs,commands/bench/bench,fddev_shared) +$(call add-objs,commands/bundle_client,fddev_shared) $(call add-objs,commands/dev,fddev_shared) $(call add-objs,commands/dump,fddev_shared) $(call add-objs,commands/flame,fddev_shared) diff --git a/src/app/shared_dev/commands/bundle_client.c b/src/app/shared_dev/commands/bundle_client.c new file mode 100644 index 0000000000..9e8b96726b --- /dev/null +++ b/src/app/shared_dev/commands/bundle_client.c @@ -0,0 +1,99 @@ +#include "../../shared/fd_config.h" +#include "../../shared/commands/run/run.h" +#include "../../../disco/tiles.h" +#include "../../../disco/topo/fd_topob.h" + +#include /* pause */ + +extern fd_topo_obj_callbacks_t * CALLBACKS[]; + +fd_topo_run_tile_t +fdctl_tile_run( fd_topo_tile_t const * tile ); + +static void +bundle_client_topo( config_t * config ) { + fd_topo_t * topo = &config->topo; + fd_topob_new( &config->topo, config->name ); + topo->max_page_size = fd_cstr_to_shmem_page_sz( config->hugetlbfs.max_page_size ); + + fd_topob_wksp( topo, "metric_in" ); + + /* Tiles */ + + fd_topob_wksp( topo, "bundle" ); + fd_topo_tile_t * bundle_tile = fd_topob_tile( topo, "bundle", "bundle", "metric_in", ULONG_MAX, 0, 1 ); + + fd_topob_wksp( topo, "sign" ); + fd_topo_tile_t * sign_tile = fd_topob_tile( topo, "sign", "sign", "metric_in", ULONG_MAX, 0, 1 ); + + fd_topob_wksp( topo, "metric" ); + fd_topo_tile_t * metric_tile = fd_topob_tile( topo, "metric", "metric", "metric_in", ULONG_MAX, 0, 0 ); + + /* Links */ + + fd_topob_link( topo, "bundle_verif", "bundle", config->tiles.verify.receive_buffer_size, FD_TPU_PARSED_MTU, 1UL ) + ->permit_no_consumers = 1; + fd_topob_link( topo, "bundle_sign", "bundle", 65536UL, 9UL, 1UL ); + fd_topob_link( topo, "sign_bundle", "bundle", 128UL, 64UL, 1UL ); + + fd_topob_tile_out( topo, "bundle", 0UL, "bundle_verif", 0UL ); + fd_topob_tile_out( topo, "bundle", 0UL, "bundle_sign", 0UL ); + fd_topob_tile_out( topo, "sign", 0UL, "sign_bundle", 0UL ); + fd_topob_tile_in( topo, "bundle", 0UL, "metric_in", "sign_bundle", 0UL, FD_TOPOB_UNRELIABLE, FD_TOPOB_UNPOLLED ); + fd_topob_tile_in( topo, "sign", 0UL, "metric_in", "bundle_sign", 0UL, FD_TOPOB_UNRELIABLE, FD_TOPOB_POLLED ); + + /* Tile config */ + + strncpy( bundle_tile->bundle.url, config->tiles.bundle.url, sizeof(bundle_tile->bundle.url) ); + bundle_tile->bundle.url_len = strnlen( config->tiles.bundle.url, 255 ); + strncpy( bundle_tile->bundle.sni, config->tiles.bundle.tls_domain_name, 256 ); + bundle_tile->bundle.sni_len = strnlen( config->tiles.bundle.tls_domain_name, 255 ); + strncpy( bundle_tile->bundle.identity_key_path, config->paths.identity_key, sizeof(bundle_tile->bundle.identity_key_path) ); + strncpy( bundle_tile->bundle.key_log_path, config->development.bundle.ssl_key_log_file, sizeof(bundle_tile->bundle.key_log_path) ); + bundle_tile->bundle.buf_sz = config->development.bundle.buffer_size_kib<<10; + bundle_tile->bundle.ssl_heap_sz = config->development.bundle.ssl_heap_size_mib<<20; + bundle_tile->bundle.keepalive_interval_nanos = (ulong)( config->tiles.bundle.keepalive_interval_seconds * 1e9f ); + bundle_tile->bundle.request_timeout_nanos = (ulong)( config->tiles.bundle.request_timeout_seconds * 1e9f ); + + strncpy( sign_tile->sign.identity_key_path, config->paths.identity_key, sizeof(sign_tile->sign.identity_key_path) ); + + if( FD_UNLIKELY( !fd_cstr_to_ip4_addr( config->tiles.metric.prometheus_listen_address, &metric_tile->metric.prometheus_listen_addr ) ) ) + FD_LOG_ERR(( "failed to parse prometheus listen address `%s`", config->tiles.metric.prometheus_listen_address )); + metric_tile->metric.prometheus_listen_port = config->tiles.metric.prometheus_listen_port; + + /* Wrap up */ + + fd_topob_finish( topo, CALLBACKS ); + fd_topo_print_log( /* stdout */ 1, topo ); +} + +static void +bundle_client_cmd_args( int * pargc, + char *** pargv, + args_t * args ) { + (void)pargc; (void)pargv; (void)args; +} + +static void +bundle_client_cmd_fn( args_t * args, + config_t * config ) { + (void)args; + fd_topo_t * topo = &config->topo; + bundle_client_topo( config ); + initialize_workspaces( config ); + initialize_stacks( config ); + fd_topo_join_workspaces( topo, FD_SHMEM_JOIN_MODE_READ_WRITE ); + + fd_topo_run_single_process( topo, 2, config->uid, config->gid, fdctl_tile_run, NULL ); + + for(;;) pause(); +} + +action_t fd_action_bundle_client = { + .name = "bundle-client", + .args = bundle_client_cmd_args, + .fn = bundle_client_cmd_fn, + .perm = NULL, + .description = "Run bundle tile in isolation", + .is_diagnostic = 1 /* allow running against live clusters */ +}; diff --git a/src/disco/bundle/Local.mk b/src/disco/bundle/Local.mk new file mode 100644 index 0000000000..a323765565 --- /dev/null +++ b/src/disco/bundle/Local.mk @@ -0,0 +1,14 @@ +ifdef FD_HAS_INT128 +$(call add-hdrs,fd_bundle_crank.h) +$(call add-objs,fd_bundle_crank,fd_disco,fd_flamenco) +$(call make-unit-test,test_bundle_crank,test_bundle_crank,fd_disco fd_flamenco fd_ballet fd_util) +$(call run-unit-test,test_bundle_crank) +endif + +$(call add-hdrs,fd_bundle_tile.h) +$(call add-objs,fd_bundle_auth fd_bundle_client,fd_disco) +$(call make-unit-test,test_bundle_client,test_bundle_client,fd_disco fd_waltz fd_flamenco fd_tango fd_ballet fd_util,-lssl -lcrypto) + +ifdef FD_HAS_SSE # implies FD_HAS_DOUBLE +$(call add-objs,fd_bundle_tile,fd_disco) +endif diff --git a/src/disco/bundle/README.md b/src/disco/bundle/README.md new file mode 100644 index 0000000000..3bb1075a9a --- /dev/null +++ b/src/disco/bundle/README.md @@ -0,0 +1,34 @@ +# Bundle tile 2.0 + +This directory contains a rewrite of the "bundle" tile. + +## Feature checklist + +**Bundle tile 1.0** + +Features supported by the old bundle tile: + +- [x] Packet delivery +- [x] Bundle delivery +- [X] gRPC over HTTP/2 connections without TLS +- [x] gRPC over HTTP/2 connections with TLS +- [x] Custom SNI +- [ ] Seccomp sandboxing +- [x] Challenge-response auth token flow +- [ ] Refresh auth token flow +- [x] Basic metrics +- [ ] Connection time out +- [ ] Reconnect back-off + +**Bundle tile 2.0** + +Features to ship in the new bundle tile: + +- [ ] TCP performance metrics +- [ ] HTTP/2 performance metrics +- [ ] gRPC performance metrics +- [ ] SSL key log file + +**Unsure** + +- [ ] Power saving (yield to OS scheduler when I/O is inactive) diff --git a/src/disco/bundle/fd_bundle_auth.c b/src/disco/bundle/fd_bundle_auth.c new file mode 100644 index 0000000000..ad90af37d8 --- /dev/null +++ b/src/disco/bundle/fd_bundle_auth.c @@ -0,0 +1,197 @@ +#include "fd_bundle_auth.h" +#include "proto/auth.pb.h" +#include "../../ballet/base58/fd_base58.h" +#include "../../ballet/nanopb/pb_decode.h" +#include "../../disco/keyguard/fd_keyguard.h" +#include "../../disco/keyguard/fd_keyguard_client.h" + +/* fd_bundle_auther_t generates auth tokens and keeps them refreshed. */ + +/* FIXME consider rewriting this with coroutines or light threads */ + +fd_bundle_auther_t * +fd_bundle_auther_init( fd_bundle_auther_t * auther ) { + *auther = (fd_bundle_auther_t) { + .state = FD_BUNDLE_AUTH_STATE_REQ_CHALLENGE, + .needs_poll = 1 + }; + return auther; +} + +void +fd_bundle_auther_handle_request_fail( fd_bundle_auther_t * auther ) { + switch( auther->state ) { + case FD_BUNDLE_AUTH_STATE_WAIT_CHALLENGE: + FD_LOG_DEBUG(( "Request for auth challenge failed, retrying" )); + auther->state = FD_BUNDLE_AUTH_STATE_REQ_CHALLENGE; + auther->needs_poll = 1; + break; + case FD_BUNDLE_AUTH_STATE_WAIT_TOKENS: + FD_LOG_DEBUG(( "Request for auth tokens failed" )); + auther->state = FD_BUNDLE_AUTH_STATE_REQ_CHALLENGE; + auther->needs_poll = 1; + break; + } +} + +static void +fd_bundle_auther_req_challenge( fd_bundle_auther_t * auther, + fd_grpc_client_t * client, + char const * host, + ulong host_len, + ushort port ) { + if( FD_UNLIKELY( fd_grpc_client_request_is_blocked( client ) ) ) return; + + auth_GenerateAuthChallengeRequest req = {0}; + req.role = auth_Role_VALIDATOR; + memcpy( req.pubkey.bytes, auther->pubkey, 32 ); + req.pubkey.size = 32; + + static char const path[] = "/auth.AuthService/GenerateAuthChallenge"; + int req_ok = fd_grpc_client_request_start( + client, + host, host_len, port, + path, sizeof(path)-1, + FD_BUNDLE_CLIENT_REQ_Auth_GenerateAuthChallenge, + &auth_GenerateAuthChallengeRequest_msg, &req, + NULL, 0 + ); + if( FD_UNLIKELY( !req_ok ) ) return; + + auther->state = FD_BUNDLE_AUTH_STATE_WAIT_CHALLENGE; + auther->needs_poll = 0; + + FD_LOG_INFO(( "Requesting bundle auth challenge" )); +} + +int +fd_bundle_auther_handle_challenge_resp( + fd_bundle_auther_t * auther, + void const * data, + ulong data_sz +) { + auther->needs_poll = 1; + + pb_istream_t istream = pb_istream_from_buffer( data, data_sz ); + auth_GenerateAuthChallengeResponse resp = auth_GenerateAuthChallengeResponse_init_default; + int decode_ok = pb_decode( &istream, &auth_GenerateAuthChallengeResponse_msg, &resp ); + if( FD_UNLIKELY( !decode_ok ) ) { + FD_LOG_WARNING(( "Protobuf decode of (auth.GenerateAuthChallengeResponse) failed" )); + goto fail; + } + if( FD_UNLIKELY( resp.challenge.size!=9UL ) ) { + FD_LOG_WARNING(( "Unexpected auth.GenerateAuthChallengeResponse challenge size: %u bytes", resp.challenge.size )); + goto fail; + } + memcpy( auther->challenge, resp.challenge.bytes, 9UL ); + + auther->state = FD_BUNDLE_AUTH_STATE_REQ_TOKENS; + FD_LOG_DEBUG(( "Got auth challenge" )); + return 1; + +fail: + auther->state = FD_BUNDLE_AUTH_STATE_REQ_CHALLENGE; + return 0; +} + +static void +fd_bundle_auther_req_tokens( fd_bundle_auther_t * auther, + fd_grpc_client_t * client, + fd_keyguard_client_t * keyguard, + char const * host, + ulong host_len, + ushort port ) { + if( FD_UNLIKELY( fd_grpc_client_request_is_blocked( client ) ) ) return; + + auth_GenerateAuthTokensRequest req = {0}; + + /* Format challenge string as '{base58(pubkey)}-{challenge}' */ + ulong enc_len; + char * p = (char *)req.challenge.bytes; + fd_base58_encode_32( auther->pubkey, &enc_len, p ); + p += enc_len; + p = fd_cstr_append_char( p, '-' ); + p = fd_cstr_append_text( p, auther->challenge, 9UL ); + req.challenge.size = (uint)( (ulong)p - (ulong)req.challenge.bytes ); + + memcpy( req.client_pubkey.bytes, auther->pubkey, 32UL ); + req.client_pubkey.size = 32UL; + + fd_keyguard_client_sign( keyguard, req.signed_challenge.bytes, (uchar const *)auther->challenge, 9UL, FD_KEYGUARD_SIGN_TYPE_PUBKEY_CONCAT_ED25519 ); + req.signed_challenge.size = 64UL; + + static char const path[] = "/auth.AuthService/GenerateAuthTokens"; + int req_ok = fd_grpc_client_request_start( + client, + host, host_len, port, + path, sizeof(path)-1, + FD_BUNDLE_CLIENT_REQ_Auth_GenerateAuthTokens, + &auth_GenerateAuthTokensRequest_msg, &req, + NULL, 0 + ); + if( FD_UNLIKELY( !req_ok ) ) return; + + auther->state = FD_BUNDLE_AUTH_STATE_WAIT_TOKENS; + auther->needs_poll = 0; + + FD_LOG_DEBUG(( "Requesting bundle auth tokens" )); +} + +int +fd_bundle_auther_handle_tokens_resp( + fd_bundle_auther_t * auther, + void const * data, + ulong data_sz +) { + pb_istream_t istream = pb_istream_from_buffer( data, data_sz ); + auth_GenerateAuthTokensResponse resp = auth_GenerateAuthTokensResponse_init_default; + int decode_ok = pb_decode( &istream, &auth_GenerateAuthTokensResponse_msg, &resp ); + if( FD_UNLIKELY( !decode_ok ) ) { + FD_LOG_WARNING(( "Protobuf decode of (auth.GenerateAuthTokensResponse) failed" )); + goto fail; + } + if( FD_UNLIKELY( !resp.has_access_token || resp.access_token.value.size==0 ) ) { + FD_LOG_WARNING(( "auth.GenerateAuthTokensResponse: missing access_token" )); + goto fail; + } + if( FD_UNLIKELY( resp.access_token.value.size > sizeof(auther->access_token) ) ) { + FD_LOG_WARNING(( "auth.GenerateAuthTokensResponse: oversz access_token: %u bytes", resp.access_token.value.size )); + goto fail; + } + + fd_memcpy( auther->access_token, resp.access_token.value.bytes, resp.access_token.value.size ); + auther->access_token_sz = (ushort)resp.access_token.value.size; + auther->state = FD_BUNDLE_AUTH_STATE_DONE_WAIT; + FD_LOG_DEBUG(( "Got auth tokens" )); + return 1; + +fail: + auther->state = FD_BUNDLE_AUTH_STATE_REQ_CHALLENGE; + auther->needs_poll = 1; + return 0; +} + +void +fd_bundle_auther_poll( fd_bundle_auther_t * auther, + fd_grpc_client_t * client, + fd_keyguard_client_t * keyguard, + char const * host, + ulong host_len, + ushort port ) { + switch( auther->state ) { + case FD_BUNDLE_AUTH_STATE_REQ_CHALLENGE: + fd_bundle_auther_req_challenge( auther, client, host, host_len, port ); + break; + case FD_BUNDLE_AUTH_STATE_REQ_TOKENS: + fd_bundle_auther_req_tokens( auther, client, keyguard, host, host_len, port ); + break; + default: + break; + } +} + +void +fd_bundle_auther_reset( fd_bundle_auther_t * auther ) { + auther->state = FD_BUNDLE_AUTH_STATE_REQ_CHALLENGE; + auther->needs_poll = 1; +} diff --git a/src/disco/bundle/fd_bundle_auth.h b/src/disco/bundle/fd_bundle_auth.h new file mode 100644 index 0000000000..fcc6a93d3d --- /dev/null +++ b/src/disco/bundle/fd_bundle_auth.h @@ -0,0 +1,73 @@ +#ifndef HEADER_fd_src_disco_bundle_fd_bundle_auth_h +#define HEADER_fd_src_disco_bundle_fd_bundle_auth_h + +#include "../../waltz/grpc/fd_grpc_client.h" +#include "../../disco/keyguard/fd_keyguard_client.h" + +struct fd_bundle_auther { + int state; + uint needs_poll : 1; + uchar pubkey[ 32 ]; + + char challenge[ 9 ]; + char access_token [ 512 ]; + ushort access_token_sz; +}; + +typedef struct fd_bundle_auther fd_bundle_auther_t; + +#define FD_BUNDLE_CLIENT_REQ_Auth_GenerateAuthChallenge 1 +#define FD_BUNDLE_CLIENT_REQ_Auth_GenerateAuthTokens 2 +//#define FD_BUNDLE_CLIENT_REQ_Auth_RefreshAccessToken 3 + +#define FD_BUNDLE_AUTH_STATE_REQ_CHALLENGE 0 +#define FD_BUNDLE_AUTH_STATE_WAIT_CHALLENGE 1 +#define FD_BUNDLE_AUTH_STATE_REQ_TOKENS 2 +#define FD_BUNDLE_AUTH_STATE_WAIT_TOKENS 3 +#define FD_BUNDLE_AUTH_STATE_DONE_WAIT 4 + +FD_PROTOTYPES_BEGIN + +fd_bundle_auther_t * +fd_bundle_auther_init( fd_bundle_auther_t * auther ); + +/* fd_bundle_auther_poll does request work. Should be called as soon as + possible if auther->needs_poll is set. Otherwise, should be called + at a low rate (once every ~100ms). */ + +void +fd_bundle_auther_poll( fd_bundle_auther_t * auther, + fd_grpc_client_t * client, + fd_keyguard_client_t * keyguard, + char const * host, + ulong host_len, + ushort port ); + +/* fd_bundle_auther_reset restarts authentication. Intended to be + called when a request fails with an auth failure. */ + +void +fd_bundle_auther_reset( fd_bundle_auther_t * auther ); + +/* Response handlers */ + +void +fd_bundle_auther_handle_request_fail( fd_bundle_auther_t * auther ); + +int +fd_bundle_auther_handle_challenge_resp( + fd_bundle_auther_t * auther, + void const * data, + ulong data_sz +); + +int +fd_bundle_auther_handle_tokens_resp( + fd_bundle_auther_t * auther, + void const * data, + ulong data_sz +); + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_disco_bundle_fd_bundle_auth_h */ diff --git a/src/disco/bundle/fd_bundle_client.c b/src/disco/bundle/fd_bundle_client.c new file mode 100644 index 0000000000..19193587d3 --- /dev/null +++ b/src/disco/bundle/fd_bundle_client.c @@ -0,0 +1,897 @@ +/* fd_bundle_client.c steps gRPC related tasks. */ + +#include "fd_bundle_auth.h" +#include "fd_bundle_tile_private.h" +#include "proto/block_engine.pb.h" +#include "proto/bundle.pb.h" +#include "proto/packet.pb.h" +#include "../fd_txn_m_t.h" +#include "../plugin/fd_plugin.h" +#include "../../waltz/h2/fd_h2_conn.h" +#include "../../ballet/base58/fd_base58.h" +#include "../../ballet/nanopb/pb_decode.h" +#include "../../util/net/fd_ip4.h" + +#include +#include +#include /* close */ +#include /* poll */ +#include /* socket */ +#include /* IPPROTO_TCP */ + +static void +fd_bundle_client_reset( fd_bundle_tile_t * ctx ) { + if( ( !!ctx->bundle_subscription_live ) | + ( !!ctx->packet_subscription_live ) ) { + /* Client was connected before but now is not. Inform user. */ + FD_LOG_WARNING(( "Disconnected from bundle server" )); + } + + if( FD_UNLIKELY( ctx->tcp_sock >= 0 ) ) { + if( FD_UNLIKELY( 0!=close( ctx->tcp_sock ) ) ) { + FD_LOG_ERR(( "close(tcp_sock=%i) failed (%i-%s)", ctx->tcp_sock, errno, fd_io_strerror( errno ) )); + } + ctx->tcp_sock = -1; + ctx->tcp_sock_connected = 0; + } + ctx->defer_reset = 0; + + ctx->builder_info_wait = 0; + ctx->packet_subscription_live = 0; + ctx->packet_subscription_wait = 0; + ctx->bundle_subscription_live = 0; + ctx->bundle_subscription_wait = 0; + +# if FD_HAS_OPENSSL + if( FD_UNLIKELY( ctx->ssl ) ) { + SSL_free( ctx->ssl ); + ctx->ssl = NULL; + } +# endif + + /* No need to free, self-contained object */ + ctx->grpc_client = NULL; + + fd_bundle_tile_backoff( ctx, fd_tickcount() ); + + fd_bundle_auther_handle_request_fail( &ctx->auther ); +} + +static int +fd_bundle_client_do_connect( fd_bundle_tile_t const * ctx ) { + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_addr.s_addr = ctx->server_ip4_addr, + .sin_port = fd_ushort_bswap( ctx->server_tcp_port ) + }; + errno = 0; + connect( ctx->tcp_sock, fd_type_pun_const( &addr ), sizeof(struct sockaddr_in) ); + return errno; +} + +/* Provided by fdctl/firedancer version.c */ +extern char const fdctl_version_string[]; + +static void +fd_bundle_client_create_conn( fd_bundle_tile_t * ctx ) { + fd_bundle_client_reset( ctx ); + + int tcp_sock = socket( AF_INET, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_TCP ); + if( FD_UNLIKELY( tcp_sock<0 ) ) { + FD_LOG_ERR(( "socket(AF_INET,SOCK_STREAM|SOCK_CLOEXEC,IPPROTO_TCP) failed (%i-%s)", errno, fd_io_strerror( errno ) )); + } + ctx->tcp_sock = tcp_sock; + + if( FD_UNLIKELY( 0!=setsockopt( tcp_sock, SOL_SOCKET, SO_RCVBUF, &ctx->so_rcvbuf, sizeof(int) ) ) ) { + FD_LOG_ERR(( "setsockopt(SOL_SOCKET,SO_RCVBUF,%i) failed (%i-%s)", ctx->so_rcvbuf, errno, fd_io_strerror( errno ) )); + } + + if( FD_UNLIKELY( fcntl( tcp_sock, F_SETFL, O_NONBLOCK )==-1 ) ) { + FD_LOG_ERR(( "fcntl(tcp_sock,F_SETFL,O_NONBLOCK) failed (%i-%s)", errno, fd_io_strerror( errno ) )); + } + + char const * scheme = "http"; +# if FD_HAS_OPENSSL + if( ctx->is_ssl ) scheme = "https"; +# endif + + FD_LOG_INFO(( "Connecting to %s://" FD_IP4_ADDR_FMT ":%hu (%.*s)", + scheme, + FD_IP4_ADDR_FMT_ARGS( ctx->server_ip4_addr ), ctx->server_tcp_port, + (int)ctx->server_fqdn_len, ctx->server_fqdn )); + + int connect_err = fd_bundle_client_do_connect( ctx ); + if( FD_UNLIKELY( connect_err ) ) { + if( FD_UNLIKELY( connect_err!=EINPROGRESS ) ) { + FD_LOG_WARNING(( "connect(tcp_sock," FD_IP4_ADDR_FMT ":%u) failed (%i-%s)", + FD_IP4_ADDR_FMT_ARGS( ctx->server_ip4_addr ), ctx->server_tcp_port, + connect_err, fd_io_strerror( connect_err ) )); + fd_bundle_client_reset( ctx ); + ctx->metrics.transport_fail_cnt++; + return; + } + } + +# if FD_HAS_OPENSSL + if( ctx->is_ssl ) { + BIO * bio = BIO_new_socket( ctx->tcp_sock, BIO_NOCLOSE ); + if( FD_UNLIKELY( !bio ) ) { + FD_LOG_ERR(( "BIO_new_socket failed" )); + } + + SSL * ssl = SSL_new( ctx->ssl_ctx ); + if( FD_UNLIKELY( !ssl ) ) { + FD_LOG_ERR(( "SSL_new failed" )); + } + + SSL_set_bio( ssl, bio, bio ); /* moves ownership of bio */ + SSL_set_connect_state( ssl ); + + if( FD_UNLIKELY( !SSL_set_tlsext_host_name( ssl, ctx->server_fqdn ) ) ) { + FD_LOG_ERR(( "SSL_set_tlsext_host_name failed" )); + } + + ctx->ssl = ssl; + } +# endif /* FD_HAS_OPENSSL */ + + ctx->grpc_client = fd_grpc_client_new( ctx->grpc_client_mem, &fd_bundle_client_grpc_callbacks, ctx->grpc_metrics, ctx, ctx->map_seed ); + if( FD_UNLIKELY( !ctx->grpc_client ) ) { + FD_LOG_CRIT(( "fd_grpc_client_new failed" )); /* unreachable */ + } + fd_grpc_client_set_version( ctx->grpc_client, fdctl_version_string, strlen( fdctl_version_string ) ); +} + +static int +fd_bundle_client_drive_io( fd_bundle_tile_t * ctx, + int * charge_busy ) { +# if FD_HAS_OPENSSL + if( ctx->is_ssl ) { + return fd_grpc_client_rxtx_ossl( ctx->grpc_client, ctx->ssl, charge_busy ); + } +# endif /* FD_HAS_OPENSSL */ + + return fd_grpc_client_rxtx_socket( ctx->grpc_client, ctx->tcp_sock, charge_busy ); +} + +static void +fd_bundle_client_request_builder_info( fd_bundle_tile_t * ctx ) { + if( FD_UNLIKELY( fd_grpc_client_request_is_blocked( ctx->grpc_client ) ) ) return; + + block_engine_BlockBuilderFeeInfoRequest req = block_engine_BlockBuilderFeeInfoRequest_init_default; + static char const path[] = "/block_engine.BlockEngineValidator/GetBlockBuilderFeeInfo"; + int req_ok = fd_grpc_client_request_start( + ctx->grpc_client, + ctx->server_fqdn, ctx->server_fqdn_len, ctx->server_tcp_port, + path, sizeof(path)-1, + FD_BUNDLE_CLIENT_REQ_Bundle_GetBlockBuilderFeeInfo, + &block_engine_BlockBuilderFeeInfoRequest_msg, &req, + ctx->auther.access_token, ctx->auther.access_token_sz + ); + if( FD_UNLIKELY( !req_ok ) ) return; + + ctx->builder_info_wait = 1; +} + +static void +fd_bundle_client_subscribe_packets( fd_bundle_tile_t * ctx ) { + if( FD_UNLIKELY( fd_grpc_client_request_is_blocked( ctx->grpc_client ) ) ) return; + + block_engine_SubscribePacketsRequest req = block_engine_SubscribePacketsRequest_init_default; + static char const path[] = "/block_engine.BlockEngineValidator/SubscribePackets"; + int req_ok = fd_grpc_client_request_start( + ctx->grpc_client, + ctx->server_fqdn, ctx->server_fqdn_len, ctx->server_tcp_port, + path, sizeof(path)-1, + FD_BUNDLE_CLIENT_REQ_Bundle_SubscribePackets, + &block_engine_SubscribePacketsRequest_msg, &req, + ctx->auther.access_token, ctx->auther.access_token_sz + ); + if( FD_UNLIKELY( !req_ok ) ) return; + + ctx->packet_subscription_wait = 1; +} + +static void +fd_bundle_client_subscribe_bundles( fd_bundle_tile_t * ctx ) { + if( FD_UNLIKELY( fd_grpc_client_request_is_blocked( ctx->grpc_client ) ) ) return; + + block_engine_SubscribeBundlesRequest req = block_engine_SubscribeBundlesRequest_init_default; + static char const path[] = "/block_engine.BlockEngineValidator/SubscribeBundles"; + int req_ok = fd_grpc_client_request_start( + ctx->grpc_client, + ctx->server_fqdn, ctx->server_fqdn_len, ctx->server_tcp_port, + path, sizeof(path)-1, + FD_BUNDLE_CLIENT_REQ_Bundle_SubscribeBundles, + &block_engine_SubscribeBundlesRequest_msg, &req, + ctx->auther.access_token, ctx->auther.access_token_sz + ); + if( FD_UNLIKELY( !req_ok ) ) return; + + ctx->bundle_subscription_wait = 1; +} + +static void +fd_bundle_client_send_ping( fd_bundle_tile_t * ctx ) { + fd_h2_conn_t * conn = fd_grpc_client_h2_conn( ctx->grpc_client ); + if( FD_UNLIKELY( !conn ) ) return; /* no conn */ + if( FD_UNLIKELY( conn->flags ) ) return; /* conn busy */ + fd_h2_rbuf_t * rbuf_tx = fd_grpc_client_rbuf_tx( ctx->grpc_client ); + + if( FD_LIKELY( fd_h2_tx_ping( conn, rbuf_tx ) ) ) { + ctx->last_ping_tx_ts = fd_tickcount(); + ctx->ping_randomize = fd_rng_ulong( ctx->rng ); + } +} + +FD_FN_PURE static int +fd_bundle_client_keepalive_due( fd_bundle_tile_t const * ctx, + long now_ticks ) { + ulong delay_min = ctx->ping_threshold_ticks>>1; + ulong delay_rng = ctx->ping_threshold_ticks & ctx->ping_randomize; + ulong delay = delay_min + delay_rng; + long deadline = ctx->last_ping_tx_ts + (long)delay; + return now_ticks >= deadline; +} + +void +fd_bundle_client_step( fd_bundle_tile_t * ctx, + int * charge_busy ) { + + /* Wait for TCP socket to connect */ + if( FD_UNLIKELY( !ctx->tcp_sock_connected ) ) { + if( FD_UNLIKELY( ctx->tcp_sock < 0 ) ) goto reconnect; + + struct pollfd pfds[1] = { + { .fd = ctx->tcp_sock, .events = POLLOUT } + }; + int poll_res = poll( pfds, 1, 0 ); + if( FD_UNLIKELY( poll_res<0 ) ) { + FD_LOG_ERR(( "poll(tcp_sock) failed (%i-%s)", errno, fd_io_strerror( errno ) )); + } + if( poll_res==0 ) return; + + if( pfds[0].revents & (POLLERR|POLLHUP) ) { + int connect_err = fd_bundle_client_do_connect( ctx ); + FD_LOG_INFO(( "Bundle gRPC connect attempt failed (%i-%s)", connect_err, fd_io_strerror( connect_err ) )); + fd_bundle_client_reset( ctx ); + ctx->metrics.transport_fail_cnt++; + *charge_busy =1 ; + return; + } + if( pfds[0].revents & POLLOUT ) { + FD_LOG_DEBUG(( "Bundle TCP socket connected" )); + ctx->tcp_sock_connected = 1; + *charge_busy =1 ; + return; + } + return; + } + + /* gRPC conn died? */ + if( FD_UNLIKELY( !ctx->grpc_client ) ) { + reconnect: + if( FD_UNLIKELY( fd_bundle_tile_should_stall( ctx, fd_tickcount() ) ) ) { + return; + } + fd_bundle_client_create_conn( ctx ); + *charge_busy = 1; + return; + } + + /* Drive I/O, SSL handshake, and any inflight requests */ + if( FD_UNLIKELY( !fd_bundle_client_drive_io( ctx, charge_busy ) || + ctx->defer_reset /* new error? */ ) ) { + fd_bundle_client_reset( ctx ); + ctx->metrics.transport_fail_cnt++; + *charge_busy = 1; + return; + } + + /* Are we ready to issue a new request? */ + if( FD_UNLIKELY( fd_grpc_client_request_is_blocked( ctx->grpc_client ) ) ) return; + long io_ts = fd_tickcount(); + if( FD_UNLIKELY( fd_bundle_tile_should_stall( ctx, io_ts ) ) ) return; + + /* Drive auth */ + if( FD_UNLIKELY( ctx->auther.needs_poll ) ) { + fd_bundle_auther_poll( &ctx->auther, ctx->grpc_client, ctx->keyguard_client, ctx->server_fqdn, ctx->server_fqdn_len, ctx->server_tcp_port ); + *charge_busy = 1; + return; + } + if( FD_UNLIKELY( ctx->auther.state!=FD_BUNDLE_AUTH_STATE_DONE_WAIT ) ) return; + + *charge_busy = 1; + + /* Request block builder info */ + if( FD_UNLIKELY( !ctx->builder_info_live && !ctx->builder_info_wait ) ) { + fd_bundle_client_request_builder_info( ctx ); + return; + } + + /* Subscribe to packets */ + if( FD_UNLIKELY( !ctx->packet_subscription_live && !ctx->packet_subscription_wait ) ) { + fd_bundle_client_subscribe_packets( ctx ); + return; + } + + /* Subscribe to bundles */ + if( FD_UNLIKELY( !ctx->bundle_subscription_live && !ctx->bundle_subscription_wait ) ) { + fd_bundle_client_subscribe_bundles( ctx ); + return; + } + + /* Send a PING */ + if( FD_UNLIKELY( fd_bundle_client_keepalive_due( ctx, io_ts ) ) ) { + fd_bundle_client_send_ping( ctx ); + return; + } + + *charge_busy = 0; +} + +void +fd_bundle_tile_backoff( fd_bundle_tile_t * ctx, + long ts_ticks ) { + uint iter = ctx->backoff_iter; + if( ts_ticks < ctx->backoff_reset ) iter = 0U; + iter++; + + /* FIXME proper backoff */ + long wait_ticks = (long)( 500e6 * fd_tempo_tick_per_ns( NULL ) ); + wait_ticks = (long)( fd_rng_ulong( ctx->rng ) & ( (1UL<backoff_until = ts_ticks + wait_ticks; + ctx->backoff_reset = ts_ticks + 2*wait_ticks; + + ctx->backoff_iter = iter; +} + +static void +fd_bundle_client_grpc_conn_established( void * app_ctx ) { + (void)app_ctx; + FD_LOG_INFO(( "Bundle gRPC connection established" )); +} + +static void +fd_bundle_client_grpc_conn_dead( void * app_ctx, + uint h2_err, + int closed_by ) { + fd_bundle_tile_t * ctx = app_ctx; + FD_LOG_INFO(( "Bundle gRPC connection closed %s (%u-%s)", + closed_by ? "by peer" : "due to error", + h2_err, fd_h2_strerror( h2_err ) )); + ctx->defer_reset = 1; +} + +/* Forwards a bundle transaction to the tango message bus. */ + +static void +fd_bundle_tile_publish_bundle_txn( + fd_bundle_tile_t * ctx, + void const * txn, + ulong txn_sz, /* <=FD_TXN_MTU */ + ulong bundle_txn_cnt +) { + if( FD_UNLIKELY( !ctx->builder_info_avail ) ) { + ctx->metrics.missing_builder_info_fail_cnt++; /* unreachable */ + return; + } + + fd_txn_m_t * txnm = fd_chunk_to_laddr( ctx->verify_out.mem, ctx->verify_out.chunk ); + *txnm = (fd_txn_m_t) { + .reference_slot = 0UL, + .payload_sz = (ushort)txn_sz, + .txn_t_sz = 0, + .block_engine = { + .bundle_id = ctx->bundle_seq, + .bundle_txn_cnt = bundle_txn_cnt, + .commission = (uchar)ctx->builder_commission + }, + }; + memcpy( txnm->block_engine.commission_pubkey, ctx->builder_pubkey, 32UL ); + fd_memcpy( fd_txn_m_payload( txnm ), txn, txn_sz ); + + ulong sz = fd_txn_m_realized_footprint( txnm, 0, 0 ); + ulong sig = 0UL; + + if( FD_UNLIKELY( !ctx->stem ) ) { + FD_LOG_CRIT(( "ctx->stem not set. This is a bug." )); + } + + ulong tspub = (ulong)fd_frag_meta_ts_comp( fd_tickcount() ); + fd_stem_publish( ctx->stem, ctx->verify_out.idx, sig, ctx->verify_out.chunk, sz, 0UL, 0UL, tspub ); + ctx->verify_out.chunk = fd_dcache_compact_next( ctx->verify_out.chunk, sz, ctx->verify_out.chunk0, ctx->verify_out.wmark ); + ctx->metrics.txn_received_cnt++; +} + +/* Forwards a regular transaction to the tango message bus. */ + +static void +fd_bundle_tile_publish_txn( + fd_bundle_tile_t * ctx, + void const * txn, + ulong txn_sz /* <=FD_TXN_MTU */ +) { + fd_txn_m_t * txnm = fd_chunk_to_laddr( ctx->verify_out.mem, ctx->verify_out.chunk ); + *txnm = (fd_txn_m_t) { + .reference_slot = 0UL, + .payload_sz = (ushort)txn_sz, + .txn_t_sz = 0, + .block_engine = { + .bundle_id = 0UL, + .bundle_txn_cnt = 1UL, + .commission = 0, + .commission_pubkey = {0} + }, + }; + fd_memcpy( fd_txn_m_payload( txnm ), txn, txn_sz ); + + ulong sz = fd_txn_m_realized_footprint( txnm, 0, 0 ); + ulong sig = 0UL; + + if( FD_UNLIKELY( !ctx->stem ) ) { + FD_LOG_CRIT(( "ctx->stem not set. This is a bug." )); + } + + ulong tspub = (ulong)fd_frag_meta_ts_comp( fd_tickcount() ); + fd_stem_publish( ctx->stem, ctx->verify_out.idx, sig, ctx->verify_out.chunk, sz, 0UL, 0UL, tspub ); + ctx->verify_out.chunk = fd_dcache_compact_next( ctx->verify_out.chunk, sz, ctx->verify_out.chunk0, ctx->verify_out.wmark ); + ctx->metrics.txn_received_cnt++; +} + +/* Called for each transaction in a bundle. Simply counts up + bundle_txn_cnt, but does not publish anything. */ + +static bool +fd_bundle_client_visit_pb_bundle_txn_preflight( + pb_istream_t * istream, + pb_field_t const * field, + void ** arg +) { + (void)istream; (void)field; + fd_bundle_tile_t * ctx = *arg; + ctx->bundle_txn_cnt++; + return true; +} + +/* Called for each transaction in a bundle. Publishes each transaction + to the tango message bus. */ + +static bool +fd_bundle_client_visit_pb_bundle_txn( + pb_istream_t * istream, + pb_field_t const * field, + void ** arg +) { + (void)field; + fd_bundle_tile_t * ctx = *arg; + + packet_Packet packet = packet_Packet_init_default; + if( FD_UNLIKELY( !pb_decode( istream, &packet_Packet_msg, &packet ) ) ) { + ctx->metrics.decode_fail_cnt++; + FD_LOG_WARNING(( "Protobuf decode of (packet.Packet) failed" )); + return false; + } + + if( FD_UNLIKELY( packet.data.size > FD_TXN_MTU ) ) { + FD_LOG_WARNING(( "Bundle server delivered an oversize transaction, ignoring" )); + return true; + } + + fd_bundle_tile_publish_bundle_txn( + ctx, + packet.data.bytes, packet.data.size, + ctx->bundle_txn_cnt + ); + + return true; +} + +/* Called for each BundleUuid in a SubscribeBundlesResponse. */ + +static bool +fd_bundle_client_visit_pb_bundle_uuid( + pb_istream_t * istream, + pb_field_t const * field, + void ** arg +) { + (void)field; + fd_bundle_tile_t * ctx = *arg; + + /* Reset bundle state */ + + ctx->bundle_txn_cnt = 0UL; + ctx->bundle_seq++; + + /* Do two decode passes. This is required because we need to know the + number of transactions in a bundle ahead of time. However, due to + the Protobuf wire encoding, we don't know the number of txns that + will come until we've parsed everything. + + First pass: Count number of bundles. */ + + pb_istream_t peek = *istream; + bundle_BundleUuid bundle = bundle_BundleUuid_init_default; + bundle.bundle.packets = (pb_callback_t) { + .funcs.decode = fd_bundle_client_visit_pb_bundle_txn_preflight, + .arg = ctx + }; + if( FD_UNLIKELY( !pb_decode( &peek, &bundle_BundleUuid_msg, &bundle ) ) ) { + ctx->metrics.decode_fail_cnt++; + FD_LOG_WARNING(( "Protobuf decode of (bundle.BundleUuid) failed: %s", peek.errmsg )); + return false; + } + + /* At this opint, ctx->bundle_txn_cnt is correctly set. + Second pass: Actually publish bundle packets */ + + bundle = (bundle_BundleUuid)bundle_BundleUuid_init_default; + bundle.bundle.packets = (pb_callback_t) { + .funcs.decode = fd_bundle_client_visit_pb_bundle_txn, + .arg = ctx + }; + + ctx->metrics.bundle_received_cnt++; + + if( FD_UNLIKELY( !pb_decode( istream, &bundle_BundleUuid_msg, &bundle ) ) ) { + ctx->metrics.decode_fail_cnt++; + FD_LOG_WARNING(( "Protobuf decode of (bundle.BundleUuid) failed (internal error): %s", istream->errmsg )); + return false; + } + + return true; +} + +/* Handle a SubscribeBundlesResponse from a SubscribeBundles gRPC call. */ + +static void +fd_bundle_client_handle_bundle_batch( + fd_bundle_tile_t * ctx, + pb_istream_t * istream +) { + if( FD_UNLIKELY( !ctx->builder_info_avail ) ) { + ctx->metrics.missing_builder_info_fail_cnt++; /* unreachable */ + return; + } + + block_engine_SubscribeBundlesResponse res = block_engine_SubscribeBundlesResponse_init_default; + res.bundles = (pb_callback_t) { + .funcs.decode = fd_bundle_client_visit_pb_bundle_uuid, + .arg = ctx + }; + if( FD_UNLIKELY( !pb_decode( istream, &block_engine_SubscribeBundlesResponse_msg, &res ) ) ) { + ctx->metrics.decode_fail_cnt++; + FD_LOG_WARNING(( "Protobuf decode of (block_engine.SubscribeBundlesResponse) failed: %s", istream->errmsg )); + return; + } +} + +/* Called for each 'Packet' (a regular transaction) of a + SubscribePacketsResponse. */ + +static bool +fd_bundle_client_visit_pb_packet( + pb_istream_t * istream, + pb_field_t const * field, + void ** arg +) { + (void)field; + fd_bundle_tile_t * ctx = *arg; + + packet_Packet packet = packet_Packet_init_default; + if( FD_UNLIKELY( !pb_decode( istream, &packet_Packet_msg, &packet ) ) ) { + ctx->metrics.decode_fail_cnt++; + FD_LOG_WARNING(( "Protobuf decode of (packet.Packet) failed" )); + return false; + } + + if( FD_UNLIKELY( packet.data.size > FD_TXN_MTU ) ) { + FD_LOG_WARNING(( "Bundle server delivered an oversize transaction, ignoring" )); + return true; + } + + fd_bundle_tile_publish_txn( ctx, packet.data.bytes, packet.data.size ); + ctx->metrics.packet_received_cnt++; + + return true; +} + +/* Handle a SubscribePacketsResponse from a SubscribePackets gRPC call. */ + +static void +fd_bundle_client_handle_packet_batch( + fd_bundle_tile_t * ctx, + pb_istream_t * istream +) { + block_engine_SubscribePacketsResponse res = block_engine_SubscribePacketsResponse_init_default; + res.batch.packets = (pb_callback_t) { + .funcs.decode = fd_bundle_client_visit_pb_packet, + .arg = ctx + }; + if( FD_UNLIKELY( !pb_decode( istream, &block_engine_SubscribePacketsResponse_msg, &res ) ) ) { + ctx->metrics.decode_fail_cnt++; + FD_LOG_WARNING(( "Protobuf decode of (block_engine.SubscribePacketsResponse) failed" )); + return; + } +} + +static void +fd_bundle_client_log_progress( fd_bundle_tile_t * ctx ) { + if( ( !!ctx->packet_subscription_live ) & + ( !!ctx->bundle_subscription_live ) & + ( !!ctx->builder_info_live ) ) { + FD_LOG_NOTICE(( "Connected to bundle server" )); + } +} + +/* Handle a BlockBuilderFeeInfoResponse from a GetBlockBuilderFeeInfo + gRPC call. */ + +static void +fd_bundle_client_handle_builder_fee_info( + fd_bundle_tile_t * ctx, + pb_istream_t * istream +) { + _Bool changed = !ctx->builder_info_avail; + + block_engine_BlockBuilderFeeInfoResponse res = block_engine_BlockBuilderFeeInfoResponse_init_default; + if( FD_UNLIKELY( !pb_decode( istream, &block_engine_BlockBuilderFeeInfoResponse_msg, &res ) ) ) { + ctx->metrics.decode_fail_cnt++; + FD_LOG_WARNING(( "Protobuf decode of (block_engine.BlockBuilderFeeInfoResponse) failed" )); + return; + } + if( FD_UNLIKELY( res.commission > 100 ) ) { + FD_LOG_WARNING(( "BlockBuilderFeeInfoResponse commission out of range (0-100): %lu, assuming 100%% commission", res.commission )); + res.commission = (uchar)100; + } + + ctx->builder_commission = (uchar)res.commission; + if( FD_UNLIKELY( !fd_base58_decode_32( res.pubkey, ctx->builder_pubkey ) ) ) { + FD_LOG_HEXDUMP_WARNING(( "Invalid pubkey in BlockBuilderFeeInfoResponse", res.pubkey, strnlen( res.pubkey, sizeof(res.pubkey) ) )); + return; + } + + ctx->builder_info_avail = 1; + ctx->builder_info_live = 1; + if( changed ) fd_bundle_client_log_progress( ctx ); +} + +static void +fd_bundle_client_grpc_tx_complete( + void * app_ctx, + ulong request_ctx +) { + (void)app_ctx; (void)request_ctx; +} + +static void +fd_bundle_client_grpc_rx_start( + void * app_ctx, + ulong request_ctx +) { + fd_bundle_tile_t * ctx = app_ctx; + _Bool changed = 0; + switch( request_ctx ) { + case FD_BUNDLE_CLIENT_REQ_Bundle_SubscribePackets: + ctx->packet_subscription_live = 1; + ctx->packet_subscription_wait = 0; + changed = 1; + break; + case FD_BUNDLE_CLIENT_REQ_Bundle_SubscribeBundles: + ctx->bundle_subscription_live = 1; + ctx->bundle_subscription_wait = 0; + changed = 1; + break; + } + + if( changed ) fd_bundle_client_log_progress( ctx ); +} + +void +fd_bundle_client_grpc_rx_msg( + void * app_ctx, + void const * protobuf, + ulong protobuf_sz, + ulong request_ctx +) { + fd_bundle_tile_t * ctx = app_ctx; + pb_istream_t istream = pb_istream_from_buffer( protobuf, protobuf_sz ); + switch( request_ctx ) { + case FD_BUNDLE_CLIENT_REQ_Auth_GenerateAuthChallenge: + if( FD_UNLIKELY( !fd_bundle_auther_handle_challenge_resp( &ctx->auther, protobuf, protobuf_sz ) ) ) { + ctx->metrics.decode_fail_cnt++; + fd_bundle_tile_backoff( ctx, fd_tickcount() ); + } + break; + case FD_BUNDLE_CLIENT_REQ_Auth_GenerateAuthTokens: + if( FD_UNLIKELY( !fd_bundle_auther_handle_tokens_resp( &ctx->auther, protobuf, protobuf_sz ) ) ) { + ctx->metrics.decode_fail_cnt++; + fd_bundle_tile_backoff( ctx, fd_tickcount() ); + } + break; + case FD_BUNDLE_CLIENT_REQ_Bundle_SubscribeBundles: + fd_bundle_client_handle_bundle_batch( ctx, &istream ); + break; + case FD_BUNDLE_CLIENT_REQ_Bundle_SubscribePackets: + fd_bundle_client_handle_packet_batch( ctx, &istream ); + break; + case FD_BUNDLE_CLIENT_REQ_Bundle_GetBlockBuilderFeeInfo: + fd_bundle_client_handle_builder_fee_info( ctx, &istream ); + break; + default: + FD_LOG_ERR(( "Received unexpected gRPC message (request_ctx=%lu)", request_ctx )); + } +} + +static void +fd_bundle_client_request_failed( fd_bundle_tile_t * ctx, + ulong request_ctx ) { + fd_bundle_tile_backoff( ctx, fd_tickcount() ); + switch( request_ctx ) { + case FD_BUNDLE_CLIENT_REQ_Auth_GenerateAuthChallenge: + case FD_BUNDLE_CLIENT_REQ_Auth_GenerateAuthTokens: + fd_bundle_auther_handle_request_fail( &ctx->auther ); + break; + } +} + +static inline int +fd_hex_unhex( int c ) { + if( c>='0' && c<='9' ) return c-'0'; + if( c>='a' && c<='f' ) return c-'a'+0xa; + if( c>='A' && c<='F' ) return c-'A'+0xa; + return -1; +} + +static ulong +fd_url_unescape( char * const msg, + ulong const len ) { + char * end = msg+len; + int state = 0; + char * dst = msg; + for( char * src=msg; srch2_status!=200 ) ) { + FD_LOG_WARNING(( "gRPC request failed (HTTP status %u)", resp->h2_status )); + fd_bundle_client_request_failed( ctx, request_ctx ); + return; + } + + resp->grpc_msg_len = (uint)fd_url_unescape( resp->grpc_msg, resp->grpc_msg_len ); + if( !resp->grpc_msg_len ) { + fd_memcpy( resp->grpc_msg, "unknown error", 13 ); + resp->grpc_msg_len = 13; + } + + switch( request_ctx ) { + case FD_BUNDLE_CLIENT_REQ_Bundle_SubscribePackets: + ctx->packet_subscription_live = 0; + fd_bundle_tile_backoff( ctx, fd_tickcount() ); + break; + case FD_BUNDLE_CLIENT_REQ_Bundle_SubscribeBundles: + ctx->bundle_subscription_live = 0; + fd_bundle_tile_backoff( ctx, fd_tickcount() ); + break; + default: + break; + } + + if( FD_UNLIKELY( resp->grpc_status!=FD_GRPC_STATUS_OK ) ) { + FD_LOG_WARNING(( "gRPC request failed (gRPC status %u-%s): %.*s", + resp->grpc_status, fd_grpc_status_cstr( resp->grpc_status ), + (int)resp->grpc_msg_len, resp->grpc_msg )); + fd_bundle_client_request_failed( ctx, request_ctx ); + if( resp->grpc_status==FD_GRPC_STATUS_UNAUTHENTICATED || + resp->grpc_status==FD_GRPC_STATUS_PERMISSION_DENIED ) { + fd_bundle_auther_reset( &ctx->auther ); + } + return; + } +} + +static void +fd_bundle_client_grpc_ping_ack( void * app_ctx ) { + fd_bundle_tile_t * ctx = app_ctx; + ctx->last_ping_rx_ts = fd_tickcount(); + ctx->metrics.ping_ack_cnt++; +} + +fd_grpc_client_callbacks_t fd_bundle_client_grpc_callbacks = { + .conn_established = fd_bundle_client_grpc_conn_established, + .conn_dead = fd_bundle_client_grpc_conn_dead, + .tx_complete = fd_bundle_client_grpc_tx_complete, + .rx_start = fd_bundle_client_grpc_rx_start, + .rx_msg = fd_bundle_client_grpc_rx_msg, + .rx_end = fd_bundle_client_grpc_rx_end, + .ping_ack = fd_bundle_client_grpc_ping_ack, +}; + +/* Decrease verbosity */ +#define DISCONNECTED FD_PLUGIN_MSG_BLOCK_ENGINE_UPDATE_STATUS_DISCONNECTED +#define CONNECTING FD_PLUGIN_MSG_BLOCK_ENGINE_UPDATE_STATUS_CONNECTING +#define CONNECTED FD_PLUGIN_MSG_BLOCK_ENGINE_UPDATE_STATUS_CONNECTED + +int +fd_bundle_client_status( fd_bundle_tile_t const * ctx ) { + if( FD_UNLIKELY( ( !ctx->tcp_sock_connected ) | + ( !ctx->grpc_client ) ) ) { + return DISCONNECTED; + } + + fd_h2_conn_t * conn = fd_grpc_client_h2_conn( ctx->grpc_client ); + if( FD_UNLIKELY( !conn ) ) { + return DISCONNECTED; /* no conn */ + } + if( FD_UNLIKELY( conn->flags & + ( FD_H2_CONN_FLAGS_DEAD | + FD_H2_CONN_FLAGS_SEND_GOAWAY ) ) ) { + return DISCONNECTED; + } + + if( FD_UNLIKELY( conn->flags & + ( FD_H2_CONN_FLAGS_CLIENT_INITIAL | + FD_H2_CONN_FLAGS_WAIT_SETTINGS_ACK_0 | + FD_H2_CONN_FLAGS_WAIT_SETTINGS_0 | + FD_H2_CONN_FLAGS_SERVER_INITIAL ) ) ) { + return CONNECTING; /* connection is not ready */ + } + + if( FD_UNLIKELY( ctx->auther.state != FD_BUNDLE_AUTH_STATE_DONE_WAIT ) ) { + return CONNECTING; /* not authenticated */ + } + + if( FD_UNLIKELY( ( !ctx->builder_info_avail ) | + ( !ctx->packet_subscription_live ) | + ( !ctx->bundle_subscription_live ) ) ) { + return CONNECTING; /* not fully connected */ + } + + long ping_timeout = (long)( 3UL * ctx->ping_threshold_ticks ); + if( FD_UNLIKELY( fd_tickcount() > ctx->last_ping_rx_ts + ping_timeout ) ) { + return DISCONNECTED; /* possible timeout */ + } + + /* As far as we know, the bundle connection is alive and well. */ + return CONNECTED; +} + +#undef DISCONNECTED +#undef CONNECTING +#undef CONNECTED diff --git a/src/disco/plugin/fd_bundle_crank.c b/src/disco/bundle/fd_bundle_crank.c similarity index 100% rename from src/disco/plugin/fd_bundle_crank.c rename to src/disco/bundle/fd_bundle_crank.c diff --git a/src/disco/plugin/fd_bundle_crank.h b/src/disco/bundle/fd_bundle_crank.h similarity index 100% rename from src/disco/plugin/fd_bundle_crank.h rename to src/disco/bundle/fd_bundle_crank.h diff --git a/src/disco/plugin/fd_bundle_crank_constants.h b/src/disco/bundle/fd_bundle_crank_constants.h similarity index 100% rename from src/disco/plugin/fd_bundle_crank_constants.h rename to src/disco/bundle/fd_bundle_crank_constants.h diff --git a/src/disco/bundle/fd_bundle_tile.c b/src/disco/bundle/fd_bundle_tile.c new file mode 100644 index 0000000000..95a5367ba6 --- /dev/null +++ b/src/disco/bundle/fd_bundle_tile.c @@ -0,0 +1,544 @@ +#include "fd_bundle_tile_private.h" +#include "../metrics/fd_metrics.h" +#include "../topo/fd_topo.h" +#include "../keyguard/fd_keyload.h" +#include "../plugin/fd_plugin.h" +#include "../../waltz/http/fd_url.h" + +#include +#include /* snprintf */ +#include /* F_SETFL */ +#include /* PROT_READ (seccomp) */ +#include /* writev */ +#include /* AF_INET */ +#include /* getaddrinfo */ + +#include "generated/fd_bundle_tile_seccomp.h" + +FD_FN_CONST static ulong +scratch_align( void ) { + return alignof(fd_bundle_tile_t); +} + +FD_FN_CONST static ulong +scratch_footprint( fd_topo_tile_t const * tile ) { + (void)tile; + ulong l = FD_LAYOUT_INIT; + l = FD_LAYOUT_APPEND( l, alignof(fd_bundle_tile_t), sizeof(fd_bundle_tile_t) ); + l = FD_LAYOUT_APPEND( l, fd_grpc_client_align(), fd_grpc_client_footprint() ); + l = FD_LAYOUT_APPEND( l, fd_alloc_align(), fd_alloc_footprint() ); + return FD_LAYOUT_FINI( l, 32 ); +} + +FD_FN_CONST static inline ulong +loose_footprint( fd_topo_tile_t const * tile ) { + (void)tile; + /* Leftover space for OpenSSL allocations */ + return 1UL<<26; /* 64 MiB */ +} + +static inline void +metrics_write( fd_bundle_tile_t * ctx ) { + FD_MCNT_SET( BUNDLE, TRANSACTION_RECEIVED, ctx->metrics.txn_received_cnt ); + FD_MCNT_SET( BUNDLE, BUNDLE_RECEIVED, ctx->metrics.bundle_received_cnt ); + FD_MCNT_SET( BUNDLE, PACKET_RECEIVED, ctx->metrics.packet_received_cnt ); + FD_MCNT_SET( BUNDLE, SHREDSTREAM_HEARTBEATS, ctx->metrics.shredstream_heartbeat_cnt ); + FD_MCNT_SET( BUNDLE, KEEPALIVES, ctx->metrics.ping_ack_cnt ); + FD_MCNT_SET( BUNDLE, ERRORS_PROTOBUF, ctx->metrics.decode_fail_cnt ); + FD_MCNT_SET( BUNDLE, ERRORS_TRANSPORT, ctx->metrics.transport_fail_cnt ); + FD_MCNT_SET( BUNDLE, ERRORS_NO_FEE_INFO, ctx->metrics.missing_builder_info_fail_cnt ); + + fd_wksp_t * wksp = fd_wksp_containing( ctx ); + fd_wksp_usage_t usage[1]; + ulong const free_tag = 0UL; + if( FD_UNLIKELY( !fd_wksp_usage( wksp, &free_tag, 1UL, usage ) ) ) { + FD_LOG_ERR(( "fd_wksp_usage failed" )); /* unreachable */ + } + FD_MGAUGE_SET( BUNDLE, HEAP_SIZE, usage->total_sz ); + FD_MGAUGE_SET( BUNDLE, HEAP_FREE_BYTES, usage->used_sz ); + + int bundle_status = fd_bundle_client_status( ctx ); + FD_MGAUGE_SET( BUNDLE, CONNECTED, bundle_status==FD_PLUGIN_MSG_BLOCK_ENGINE_UPDATE_STATUS_CONNECTED ); + ctx->bundle_status_recent = (uchar)bundle_status; +} + +static void +during_housekeeping( fd_bundle_tile_t * ctx ) { + if( FD_UNLIKELY( fd_keyswitch_state_query( ctx->keyswitch )==FD_KEYSWITCH_STATE_SWITCH_PENDING ) ) { + ctx->identity_switched = 1; + fd_memcpy( ctx->auther.pubkey, ctx->keyswitch->bytes, 32UL ); + fd_keyswitch_state( ctx->keyswitch, FD_KEYSWITCH_STATE_COMPLETED ); + } +} + +static void +fd_bundle_tile_publish_block_engine_update( + fd_bundle_tile_t * ctx, + fd_stem_context_t * stem +) { + fd_plugin_msg_block_engine_update_t * update = + fd_chunk_to_laddr( ctx->plugin_out.mem, ctx->plugin_out.chunk ); + memset( update, 0, sizeof(fd_plugin_msg_block_engine_update_t) ); + + strncpy( update->name, "jito", sizeof(update->name) ); + + snprintf( update->url, sizeof(update->url), "%s://%.*s:%u", + ctx->is_ssl ? "https" : "http", + (int)ctx->server_fqdn_len, + ctx->server_fqdn, + ctx->server_tcp_port ); + + update->status = (uchar)ctx->bundle_status_recent; + + ulong tspub = (ulong)fd_frag_meta_ts_comp( fd_tickcount() ); + fd_stem_publish( + stem, + ctx->plugin_out.idx, + FD_PLUGIN_MSG_BLOCK_ENGINE_UPDATE, + ctx->plugin_out.chunk, + sizeof(fd_plugin_msg_block_engine_update_t), + 0UL, /* ctl */ + 0UL, /* seq */ + tspub + ); + ctx->plugin_out.chunk = fd_dcache_compact_next( ctx->plugin_out.chunk, sizeof(fd_plugin_msg_block_engine_update_t), ctx->plugin_out.chunk0, ctx->plugin_out.wmark ); +} + +static void +after_credit( fd_bundle_tile_t * ctx, + fd_stem_context_t * stem, + int * opt_poll_in, + int * charge_busy ) { + (void)opt_poll_in; + if( FD_UNLIKELY( !ctx->stem ) ) ctx->stem = stem; + fd_bundle_client_step( ctx, charge_busy ); + + if( ctx->plugin_out.mem ) { + if( FD_UNLIKELY( ctx->bundle_status_recent != ctx->bundle_status_plugin ) ) { + fd_bundle_tile_publish_block_engine_update( ctx, stem ); + ctx->bundle_status_plugin = (uchar)ctx->bundle_status_recent; + *charge_busy = 1; + } + } +} + +static void +resolve_url( fd_url_t * url_, + char const * url_str, + ulong url_str_len, + uint * ip4_addr, + ushort * tcp_port, + _Bool * is_ssl ) { + + /* Parse URL */ + + int url_err[1]; + fd_url_t * url = fd_url_parse_cstr( url_, url_str, url_str_len, url_err ); + if( FD_UNLIKELY( !url ) ) { + switch( *url_err ) { + scheme_err: + case FD_URL_ERR_SCHEME: + FD_LOG_ERR(( "Invalid [tiles.bundle.url] `%.*s`: must start with `http://` or `https://`", (int)url_str_len, url_str )); + break; + case FD_URL_ERR_HOST_OVERSZ: + FD_LOG_ERR(( "Invalid [tiles.bundle.url] `%.*s`: domain name is too long", (int)url_str_len, url_str )); + break; + default: + FD_LOG_ERR(( "Invalid [tiles.bundle.url] `%.*s`", (int)url_str_len, url_str )); + break; + } + } + + /* FIXME the URL scheme path technically shouldn't contain slashes */ + if( url->scheme_len==8UL && fd_memeq( url->scheme, "https://", 8UL ) ) { + *is_ssl = 1; + } else if( url->scheme_len==7UL && fd_memeq( url->scheme, "http://", 7UL ) ) { + *is_ssl = 0; + } else { + goto scheme_err; + } + + /* Parse port number */ + + *tcp_port = 443; + if( url->port_len ) { + if( FD_UNLIKELY( url->port_len > 5 ) ) { + invalid_port: + FD_LOG_ERR(( "Invalid [tiles.bundle.url] `%.*s`: invalid port number", (int)url_str_len, url_str )); + } + + char port_cstr[6]; + fd_cstr_fini( fd_cstr_append_text( fd_cstr_init( port_cstr ), url->port, url->port_len ) ); + ulong port_no = fd_cstr_to_ulong( port_cstr ); + if( FD_UNLIKELY( !port_no || port_no>USHORT_MAX ) ) goto invalid_port; + + *tcp_port = (ushort)port_no; + } + + /* Resolve domain */ + + if( FD_UNLIKELY( url->host_len > 255 ) ) { + FD_LOG_CRIT(( "Invalid url->host_len" )); /* unreachable */ + } + char host_cstr[ 256 ]; + fd_cstr_fini( fd_cstr_append_text( fd_cstr_init( host_cstr ), url->host, url->host_len ) ); + + struct addrinfo hints, *res; + memset( &hints, 0, sizeof(hints) ); + hints.ai_family = AF_INET; + + int err = getaddrinfo( host_cstr, NULL, &hints, &res ); + if( FD_UNLIKELY( err ) ) { + FD_LOG_ERR(( "getaddrinfo `%s` failed (%d-%s)", host_cstr, err, gai_strerror( err ) )); + } + + *ip4_addr = ((struct sockaddr_in *)res->ai_addr)->sin_addr.s_addr; + freeaddrinfo( res ); +} + +static void +fd_bundle_tile_resolve_endpoint( fd_bundle_tile_t * ctx, + fd_topo_tile_t const * tile ) { + fd_url_t url[1]; + _Bool is_ssl = 0; + resolve_url( + url, + tile->bundle.url, tile->bundle.url_len, + &ctx->server_ip4_addr, + &ctx->server_tcp_port, + &is_ssl + ); + if( FD_UNLIKELY( url->host_len > 255 ) ) { + FD_LOG_CRIT(( "Invalid url->host_len" )); /* unreachable */ + } + fd_cstr_fini( fd_cstr_append_text( fd_cstr_init( ctx->server_fqdn ), url->host, url->host_len ) ); + ctx->server_fqdn_len = url->host_len; + + ctx->is_ssl = !!is_ssl; +#if !FD_HAS_OPENSSL + if( FD_UNLIKELY( is_ssl ) ) { + FD_LOG_ERR(( "This build does not include OpenSSL. To install OpenSSL, re-run ./deps.sh and do a clean re build." )); + } +#endif +} + +#if FD_HAS_OPENSSL + +/* OpenSSL allows us to specify custom memory allocation functions, + which we want to point to an fd_alloc_t, but it does not let us use a + context object. Instead we stash it in this thread local, which is + OK because the parent workspace exists for the duration of the SSL + context, and the process only has one thread. + + Currently fd_alloc doesn't support realloc, so it's implemented on + top of malloc and free, and then also it doesn't support getting the + size of an allocation from the pointer, which we need for realloc, so + we pad each alloc by 8 bytes and stuff the size into the first 8 + bytes. */ +static FD_TL fd_alloc_t * fd_quic_ssl_mem_function_ctx = NULL; + +static void * +crypto_malloc( ulong num, + char const * file, + int line ) { + (void)file; (void)line; + void * result = fd_alloc_malloc( fd_quic_ssl_mem_function_ctx, 16UL, num + 8UL ); + if( FD_UNLIKELY( !result ) ) { + FD_MCNT_INC( BUNDLE, ERRORS_SSL_ALLOC, 1UL ); + return NULL; + } + *(ulong *)result = num; + return (uchar *)result + 8UL; +} + +static void +crypto_free( void * addr, + char const * file, + int line ) { + (void)file; + (void)line; + + if( FD_UNLIKELY( !addr ) ) return; + fd_alloc_free( fd_quic_ssl_mem_function_ctx, (uchar *)addr - 8UL ); +} + +static void * +crypto_realloc( void * addr, + ulong num, + char const * file, + int line ) { + (void)file; + (void)line; + + if( FD_UNLIKELY( !addr ) ) return crypto_malloc( num, file, line ); + if( FD_UNLIKELY( !num ) ) { + crypto_free( addr, file, line ); + return NULL; + } + + void * new = fd_alloc_malloc( fd_quic_ssl_mem_function_ctx, 16UL, num + 8UL ); + if( FD_UNLIKELY( !new ) ) return NULL; + + ulong old_num = *(ulong *)( (uchar *)addr - 8UL ); + fd_memcpy( (uchar*)new + 8, (uchar*)addr, fd_ulong_min( old_num, num ) ); + fd_alloc_free( fd_quic_ssl_mem_function_ctx, (uchar *)addr - 8UL ); + *(ulong *)new = num; + return (uchar*)new + 8UL; +} + +static void +fd_ossl_keylog_callback( SSL const * ssl, + char const * line ) { + SSL_CTX * ssl_ctx = SSL_get_SSL_CTX( ssl ); + fd_bundle_tile_t * ctx = SSL_CTX_get_ex_data( ssl_ctx, 0 ); + ulong line_len = strlen( line ); + struct iovec iovs[2] = { + { .iov_base=(void *)line, .iov_len=line_len }, + { .iov_base=(void *)"\n", .iov_len=1UL } + }; + if( FD_UNLIKELY( writev( ctx->keylog_fd, iovs, 2 )!=(long)line_len+1 ) ) { + FD_LOG_WARNING(( "write(keylog) failed (%i-%s)", errno, fd_io_strerror( errno ) )); + } +} + +static void +fd_bundle_tile_init_openssl( fd_bundle_tile_t * ctx, + void * alloc_mem ) { + fd_alloc_t * alloc = fd_alloc_join( fd_alloc_new( alloc_mem, 1UL ), 1UL ); + if( FD_UNLIKELY( !alloc ) ) { + FD_LOG_ERR(( "fd_alloc_new failed" )); + } + ctx->ssl_alloc = alloc; + fd_quic_ssl_mem_function_ctx = alloc; + + if( FD_UNLIKELY( !CRYPTO_set_mem_functions( crypto_malloc, crypto_realloc, crypto_free ) ) ) { + FD_LOG_ERR(( "CRYPTO_set_mem_functions failed" )); + } + + OPENSSL_init_ssl( + OPENSSL_INIT_LOAD_SSL_STRINGS | + OPENSSL_INIT_LOAD_CRYPTO_STRINGS | + OPENSSL_INIT_NO_LOAD_CONFIG, + NULL + ); + + SSL_CTX * ssl_ctx = SSL_CTX_new( TLS_client_method() ); + if( FD_UNLIKELY( !ssl_ctx ) ) { + FD_LOG_ERR(( "SSL_CTX_new failed" )); + } + + if( FD_UNLIKELY( !SSL_CTX_set_ex_data( ssl_ctx, 0, ctx ) ) ) { + FD_LOG_ERR(( "SSL_CTX_set_ex_data failed" )); + } + + if( FD_UNLIKELY( !SSL_CTX_set_mode( ssl_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE|SSL_MODE_AUTO_RETRY ) ) ) { + FD_LOG_ERR(( "SSL_CTX_set_mode failed" )); + } + + if( FD_UNLIKELY( !SSL_CTX_set_min_proto_version( ssl_ctx, TLS1_3_VERSION ) ) ) { + FD_LOG_ERR(( "SSL_CTX_set_min_proto_version(ssl_ctx,TLS1_3_VERSION) failed" )); + } + + if( FD_UNLIKELY( 0!=SSL_CTX_set_alpn_protos( ssl_ctx, (const unsigned char *)"\x02h2", 3 ) ) ) { + FD_LOG_ERR(( "SSL_CTX_set_alpn_protos failed" )); + } + + if( FD_LIKELY( ctx->keylog_fd >= 0 ) ) { + SSL_CTX_set_keylog_callback( ssl_ctx, fd_ossl_keylog_callback ); + } + + ctx->ssl_ctx = ssl_ctx; +} + +#endif /* FD_HAS_OPENSSL */ + +static void +privileged_init( fd_topo_t * topo, + fd_topo_tile_t * tile ) { + void * scratch = fd_topo_obj_laddr( topo, tile->tile_obj_id ); + + FD_SCRATCH_ALLOC_INIT( l, scratch ); + fd_bundle_tile_t * ctx = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_bundle_tile_t), sizeof(fd_bundle_tile_t) ); + void * grpc_mem = FD_SCRATCH_ALLOC_APPEND( l, fd_grpc_client_align(), fd_grpc_client_footprint() ); + void * alloc_mem = FD_SCRATCH_ALLOC_APPEND( l, fd_alloc_align(), fd_alloc_footprint() ); + ulong scratch_end = FD_SCRATCH_ALLOC_FINI( l, scratch_align() ); + (void)alloc_mem; /* potentially unused */ + + if( FD_UNLIKELY( (ulong)ctx != (ulong)scratch ) ) { + FD_LOG_CRIT(( "Invalid bundle tile scratch alignment" )); /* unreachable */ + } + if( FD_UNLIKELY( scratch_end - (ulong)scratch > scratch_footprint( tile ) ) ) { + FD_LOG_CRIT(( "Bundle tile scratch overflow" )); /* unreachable */ + } + + memset( ctx, 0, sizeof(fd_bundle_tile_t) ); + ctx->grpc_client_mem = grpc_mem; + ctx->tcp_sock = -1; + + fd_bundle_auther_init( &ctx->auther ); + uchar const * public_key = fd_keyload_load( tile->bundle.identity_key_path, 1 /* public key only */ ); + fd_memcpy( ctx->auther.pubkey, public_key, 32UL ); + + /* DNS resolution does arbitrary syscalls and system ops, therefore + has to be run outside the sandbox. */ + fd_bundle_tile_resolve_endpoint( ctx, tile ); + + /* Override server name indication */ + if( FD_UNLIKELY( tile->bundle.sni_len ) ) { + fd_cstr_fini( fd_cstr_append_text( fd_cstr_init( ctx->server_fqdn ), tile->bundle.sni, tile->bundle.sni_len ) ); + ctx->server_fqdn_len = tile->bundle.sni_len; + } + +# if FD_HAS_OPENSSL + + ctx->keylog_fd = -1; + if( FD_UNLIKELY( tile->bundle.key_log_path[0] ) ) { + ctx->keylog_fd = open( tile->bundle.key_log_path, O_WRONLY|O_APPEND|O_CREAT, 0644 ); + if( FD_UNLIKELY( ctx->keylog_fd < 0 ) ) { + FD_LOG_ERR(( "open(%s) failed (%i-%s)", tile->bundle.key_log_path, errno, fd_io_strerror( errno ) )); + } + } + + /* OpenSSL goes and tries to read files and allocate memory and + other dumb things on a thread local basis, so we need a special + initializer to do it before seccomp happens in the process. */ + fd_bundle_tile_init_openssl( ctx, alloc_mem ); + +# endif /* FD_HAS_OPENSSL */ + + /* Random seed for header hashmap */ + if( FD_UNLIKELY( !fd_rng_secure( &ctx->map_seed, sizeof(ulong) ) ) ) { + FD_LOG_CRIT(( "fd_rng_secure failed" )); + } + + /* Random seed for timing RNG */ + uint rng_seed; + if( FD_UNLIKELY( !fd_rng_secure( &rng_seed, sizeof(uint) ) ) ) { + FD_LOG_CRIT(( "fd_rng_secure failed" )); + } + if( FD_UNLIKELY( !fd_rng_join( fd_rng_new( &ctx->rng, rng_seed, 0UL ) ) ) ) { + FD_LOG_CRIT(( "fd_rng_join failed" )); /* unreachable */ + } +} + +static fd_bundle_out_ctx_t +bundle_out_link( fd_topo_t const * topo, + fd_topo_link_t const * link, + ulong out_link_idx ) { + fd_bundle_out_ctx_t out = {0}; + out.idx = out_link_idx; + out.mem = topo->workspaces[ topo->objs[ link->dcache_obj_id ].wksp_id ].wksp; + out.chunk0 = fd_dcache_compact_chunk0( out.mem, link->dcache ); + out.wmark = fd_dcache_compact_wmark ( out.mem, link->dcache, link->mtu ); + out.chunk = out.chunk0; + return out; +} + +static void +unprivileged_init( fd_topo_t * topo, + fd_topo_tile_t * tile ) { + fd_bundle_tile_t * ctx = fd_topo_obj_laddr( topo, tile->tile_obj_id ); + if( FD_UNLIKELY( tile->kind_id!=0 ) ) { + FD_LOG_ERR(( "There can only be one bundle tile" )); + } + + ulong sign_in_idx = fd_topo_find_tile_in_link( topo, tile, "sign_bundle", tile->kind_id ); + if( FD_UNLIKELY( sign_in_idx==ULONG_MAX ) ) FD_LOG_ERR(( "Missing sign_bundle link" )); + fd_topo_link_t const * sign_in = &topo->links[ tile->in_link_id[ sign_in_idx ] ]; + + ulong sign_out_idx = fd_topo_find_tile_out_link( topo, tile, "bundle_sign", tile->kind_id ); + if( FD_UNLIKELY( sign_out_idx==ULONG_MAX ) ) FD_LOG_ERR(( "Missing bundle_sign link" )); + fd_topo_link_t const * sign_out = &topo->links[ tile->out_link_id[ sign_out_idx ] ]; + + if( FD_UNLIKELY( !fd_keyguard_client_join( fd_keyguard_client_new( + ctx->keyguard_client, + sign_out->mcache, + sign_out->dcache, + sign_in->mcache, + sign_in->dcache + ) ) ) ) { + FD_LOG_ERR(( "fd_keyguard_client_join failed" )); /* unreachable */ + } + + ctx->identity_switched = 0; + ctx->keyswitch = fd_keyswitch_join( fd_topo_obj_laddr( topo, tile->keyswitch_obj_id ) ); + FD_TEST( ctx->keyswitch ); + + + ulong verify_out_idx = fd_topo_find_tile_out_link( topo, tile, "bundle_verif", tile->kind_id ); + if( FD_UNLIKELY( verify_out_idx==ULONG_MAX ) ) FD_LOG_ERR(( "Missing bundle_verif link" )); + ctx->verify_out = bundle_out_link( topo, &topo->links[ tile->out_link_id[ verify_out_idx ] ], verify_out_idx ); + + ulong plugin_out_idx = fd_topo_find_tile_out_link( topo, tile, "bundle_plugi", tile->kind_id ); + if( plugin_out_idx!=ULONG_MAX ) { + ctx->plugin_out = bundle_out_link( topo, &topo->links[ tile->out_link_id[ plugin_out_idx ] ], plugin_out_idx ); + } else { + ctx->plugin_out = (fd_bundle_out_ctx_t){ .idx=ULONG_MAX }; + } + + /* Set socket receive buffer size */ + ulong so_rcvbuf = tile->bundle.buf_sz + 65536UL; + if( so_rcvbuf > INT_MAX ) FD_LOG_ERR(( "Invalid [development.bundle.buffer_size_kib]: too large" )); + ctx->so_rcvbuf = (int)so_rcvbuf; + + /* Set idle ping timer */ + ctx->ping_threshold_ticks = fd_ulong_pow2_up( (ulong) + ( (double)tile->bundle.keepalive_interval_nanos * fd_tempo_tick_per_ns( NULL ) ) ); + ctx->ping_randomize = fd_rng_ulong( ctx->rng ); + + /* Force tile to output a plugin message on startup */ + ctx->bundle_status_plugin = 127; + ctx->bundle_status_recent = FD_PLUGIN_MSG_BLOCK_ENGINE_UPDATE_STATUS_DISCONNECTED; +} + +static ulong +populate_allowed_seccomp( fd_topo_t const * topo, + fd_topo_tile_t const * tile, + ulong out_cnt, + struct sock_filter * out ) { + (void)topo; (void)tile; + populate_sock_filter_policy_fd_bundle_tile( + out_cnt, out, (uint)fd_log_private_logfile_fd() ); + return sock_filter_policy_fd_bundle_tile_instr_cnt; +} + +static ulong +populate_allowed_fds( fd_topo_t const * topo, + fd_topo_tile_t const * tile, + ulong out_fds_cnt, + int * out_fds ) { + (void)topo; (void)tile; + + if( FD_UNLIKELY( out_fds_cnt<2UL ) ) FD_LOG_ERR(( "out_fds_cnt %lu", out_fds_cnt )); + + ulong out_cnt = 0UL; + out_fds[ out_cnt++ ] = 2; /* stderr */ + if( FD_LIKELY( -1!=fd_log_private_logfile_fd() ) ) + out_fds[ out_cnt++ ] = fd_log_private_logfile_fd(); /* logfile */ + return out_cnt; +} + +#define STEM_BURST (5UL) + +#define STEM_CALLBACK_CONTEXT_TYPE fd_bundle_tile_t +#define STEM_CALLBACK_CONTEXT_ALIGN alignof(fd_bundle_tile_t) + +#define STEM_CALLBACK_DURING_HOUSEKEEPING during_housekeeping +#define STEM_CALLBACK_METRICS_WRITE metrics_write +#define STEM_CALLBACK_AFTER_CREDIT after_credit + +#include "../stem/fd_stem.c" + +fd_topo_run_tile_t fd_tile_bundle = { + .name = "bundle", + .populate_allowed_seccomp = populate_allowed_seccomp, + .populate_allowed_fds = populate_allowed_fds, + .scratch_align = scratch_align, + .scratch_footprint = scratch_footprint, + .loose_footprint = loose_footprint, + .privileged_init = privileged_init, + .unprivileged_init = unprivileged_init, + .run = stem_run, + .rlimit_file_cnt = 64, + .keep_host_networking = 1 +}; diff --git a/src/disco/bundle/fd_bundle_tile.h b/src/disco/bundle/fd_bundle_tile.h new file mode 100644 index 0000000000..b053db28c5 --- /dev/null +++ b/src/disco/bundle/fd_bundle_tile.h @@ -0,0 +1,24 @@ +#ifndef HEADER_fd_src_disco_bundle_fd_bundle_tile_h +#define HEADER_fd_src_disco_bundle_fd_bundle_tile_h + +/* fd_bundle_tile.h provides a bundle client tile. + + - Requires HTTP/2 over TLS connections + - Uses TCP sockets + - Uses OpenSSL to drive socket I/O, and provide handshake and record + layers. + - Uses Firedancer's fd_h2 and fd_grpc for HTTP/2 and gRPC logic. + - Does busy polling (no power saving features) */ + +#include "../topo/fd_topo.h" + +struct fd_bundle_tile; +typedef struct fd_bundle_tile fd_bundle_tile_t; + +FD_PROTOTYPES_BEGIN + +extern fd_topo_run_tile_t fd_tile_bundle; + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_disco_bundle_fd_bundle_tile_h */ diff --git a/src/disco/bundle/fd_bundle_tile.seccomppolicy b/src/disco/bundle/fd_bundle_tile.seccomppolicy new file mode 100644 index 0000000000..8d1fa78689 --- /dev/null +++ b/src/disco/bundle/fd_bundle_tile.seccomppolicy @@ -0,0 +1,67 @@ +# logfile_fd: It can be disabled by configuration, but typically tiles +# will open a log file on boot and write all messages there. +unsigned int logfile_fd + +# bundle: Read from TCP connection (HTTPS) +read + +# bundle: Read from TCP connection (HTTP) +recvmsg: (eq (arg 2) "MSG_NOSIGNAL|MSG_DONTWAIT") + +# bundle: Write to TCP connection (HTTPS) +# +# logging: all log messages are written to a file and/or pipe +# +# 'WARNING' and above are written to the STDERR pipe, while all +# messages are always written to the log file. +# +# arg 0 is the file descriptor to write to. The boot process ensures +# that descriptor 2 is always STDERR. +write + +# bundle: Write to TCP connection (HTTP) +sendmsg: (eq (arg 2) "MSG_NOSIGNAL|MSG_DONTWAIT") + +# logging: 'WARNING' and above fsync the logfile to disk immediately +# +# arg 0 is the file descriptor to fsync. +fsync: (eq (arg 0) logfile_fd) + +# bundle: TCP connection to the bundle server needs to be established. +socket: (and (eq (arg 0) "AF_INET") + (eq (arg 1) "SOCK_STREAM|SOCK_CLOEXEC") + (eq (arg 2) "IPPROTO_TCP")) +connect + +# bundle: TCP connection is closed if there is an error, and a new one +# is reopened +shutdown: (eq (arg 1) "SHUT_WR") +close + +# bundle: Make TCP connection non-blocking +fcntl: (and (eq (arg 1) "F_SETFL") + (eq (arg 2) "O_NONBLOCK")) + +# bundle: Wait for TCP connection to be established +poll: (and (eq (arg 1) 1) + (eq (arg 2) 0)) + +# bundle: configure TCP socket +setsockopt: (and (eq (arg 1) SOL_SOCKET) + (eq (arg 2) SO_RCVBUF)) + +# openssl: RAND_bytes requires getpid +# +# QUIC uses OpenSSL RAND_bytes to generate randomness, which it uses for +# picking connection IDs. The OpenSSL implementation calls getpid() as +# an implementation detail, they save the PID in a global and reseed the +# RNG if the PID ever changes (the process was forked). We don't need +# this logic since our process can't fork. +getpid + +# openssl: RAND_bytes requires getrandom +# +# QUIC uses OpenSSL RAND_bytes to generate randomness, which it uses for +# picking connection IDs. The OpenSSL implementation calls getrandom +# internally for periodically reseeding the RNG. +getrandom diff --git a/src/disco/bundle/fd_bundle_tile_private.h b/src/disco/bundle/fd_bundle_tile_private.h new file mode 100644 index 0000000000..74acf23337 --- /dev/null +++ b/src/disco/bundle/fd_bundle_tile_private.h @@ -0,0 +1,198 @@ +#ifndef HEADER_fd_src_disco_bundle_fd_bundle_tile_private_h +#define HEADER_fd_src_disco_bundle_fd_bundle_tile_private_h + +#include "fd_bundle_auth.h" +#include "../stem/fd_stem.h" +#include "../keyguard/fd_keyswitch.h" +#include "../keyguard/fd_keyguard_client.h" +#include "../../waltz/grpc/fd_grpc_client.h" +#include "../../util/alloc/fd_alloc.h" + +#if FD_HAS_OPENSSL +#include /* SSL_CTX */ +#endif + +struct fd_bundle_out_ctx { + ulong idx; + fd_wksp_t * mem; + ulong chunk0; + ulong wmark; + ulong chunk; +}; + +typedef struct fd_bundle_out_ctx fd_bundle_out_ctx_t; + +/* fd_bundle_metrics_t contains private metric counters. These get + published to fd_metrics periodically. */ + +struct fd_bundle_metrics { + ulong txn_received_cnt; + ulong bundle_received_cnt; + ulong packet_received_cnt; + ulong shredstream_heartbeat_cnt; + ulong ping_ack_cnt; + + ulong decode_fail_cnt; + ulong transport_fail_cnt; + ulong missing_builder_info_fail_cnt; +}; + +typedef struct fd_bundle_metrics fd_bundle_metrics_t; + +/* fd_bundle_tile_t is the context object provided to callbacks from the + mux tile, and contains all state needed to progress the tile. */ + +struct fd_bundle_tile { + /* Key switch */ + fd_keyswitch_t * keyswitch; + uint identity_switched : 1; + + /* Key guard */ + fd_keyguard_client_t keyguard_client[1]; + + uint is_ssl : 1; +# if FD_HAS_OPENSSL + /* OpenSSL */ + SSL_CTX * ssl_ctx; + SSL * ssl; + fd_alloc_t * ssl_alloc; + uint skip_cert_verify : 1; + int keylog_fd; +# endif /* FD_HAS_OPENSSL */ + + /* Config */ + char server_fqdn[ 256 ]; /* cstr */ + ulong server_fqdn_len; + uint server_ip4_addr; + ushort server_tcp_port; + + /* TCP socket */ + int tcp_sock; + int so_rcvbuf; + uint tcp_sock_connected : 1; + uint defer_reset : 1; + + /* Keepalive via HTTP/2 PINGs (randomized) */ + long last_ping_tx_ts; /* last TX tickcount */ + long last_ping_rx_ts; /* last RX tickcount */ + ulong ping_randomize; /* random 64 bits */ + ulong ping_threshold_ticks; /* avg keepalive timeout in ticks, 2^n-1 */ + + /* gRPC client */ + void * grpc_client_mem; + fd_grpc_client_t * grpc_client; + fd_grpc_client_metrics_t grpc_metrics[1]; + ulong map_seed; + + /* Bundle authenticator */ + fd_bundle_auther_t auther; + + /* Bundle block builder info */ + uchar builder_pubkey[ 32 ]; + uchar builder_commission; /* in [0,100] (percent) */ + uchar builder_info_avail : 1; /* Block builder info available? (potentially stale) */ + uchar builder_info_live : 1; /* Block builder info recent enough? */ + uchar builder_info_wait : 1; /* Request already in-flight? */ + + /* Bundle subscriptions */ + uchar packet_subscription_live : 1; /* Want to subscribe to a stream? */ + uchar packet_subscription_wait : 1; /* Request already in-flight? */ + uchar bundle_subscription_live : 1; + uchar bundle_subscription_wait : 1; + + /* Bundle state */ + ulong bundle_seq; + ulong bundle_txn_cnt; + + /* Error backoff */ + fd_rng_t rng[1]; + uint backoff_iter; + long backoff_until; + long backoff_reset; + + /* Stem publish */ + fd_stem_context_t * stem; + fd_bundle_out_ctx_t verify_out; + fd_bundle_out_ctx_t plugin_out; + + /* App metrics */ + fd_bundle_metrics_t metrics; + + /* Check engine light */ + uchar bundle_status_recent; /* most recently observed 'check engine light' */ + uchar bundle_status_plugin; /* last 'plugin' update written */ +}; + +typedef struct fd_bundle_tile fd_bundle_tile_t; + +/* Define 'request_ctx' IDs to identify different types of gRPC calls */ + +#define FD_BUNDLE_CLIENT_REQ_Bundle_SubscribePackets 4 +#define FD_BUNDLE_CLIENT_REQ_Bundle_SubscribeBundles 5 +#define FD_BUNDLE_CLIENT_REQ_Bundle_GetBlockBuilderFeeInfo 6 + +FD_PROTOTYPES_BEGIN + +/* fd_bundle_client_grpc_callbacks provides callbacks for grpc_client. */ + +extern fd_grpc_client_callbacks_t fd_bundle_client_grpc_callbacks; + +/* fd_bundle_client_step is an all-in-one routine to drive client logic. + As long as the tile calls this periodically, the client will + reconnect to the bundle server, authenticate, and subscribe to + packets and bundles. */ + +void +fd_bundle_client_step( fd_bundle_tile_t * bundle, + int * charge_busy ); + +/* fd_bundle_tile_backoff is called whenever an error occurs. Stalls + forward progress for a randomized amount of time to prevent error + floods. */ + +void +fd_bundle_tile_backoff( fd_bundle_tile_t * ctx, + long tickcount ); + +/* fd_bundle_tile_should_stall returns 1 if forward progress should be + temporarily prevented due to an error. */ + +FD_FN_PURE static inline int +fd_bundle_tile_should_stall( fd_bundle_tile_t const * ctx, + long tickcount ) { + return tickcount < ctx->backoff_until; +} + +/* fd_bundle_client_grpc_rx_msg is called by grpc_client when a gRPC + message arrives (unary or server-streaming response). */ + +void +fd_bundle_client_grpc_rx_msg( + void * app_ctx, /* (fd_bundle_tile_t *) */ + void const * protobuf, + ulong protobuf_sz, + ulong request_ctx /* FD_BUNDLE_CLIENT_REQ_{...} */ +); + +/* fd_bundle_client_status provides a "check engine light". + + Returns 0 if the client has recently failed and is currently backing + off from a reconnect attempt. + + Returns 1 if the client is currently reconnecting. + + Returns 2 if all of the following conditions are met: + - TCP socket is alive + - SSL session is not in an error state + - HTTP/2 connection is established (SETTINGS exchange done) + - gRPC bundle and packet subscriptions are live + - HTTP/2 PING exchange was done recently + + Return codes are compatible with FD_PLUGIN_MSG_BLOCK_ENGINE_UPDATE_STATUS_{...}. */ + +int +fd_bundle_client_status( fd_bundle_tile_t const * ctx ); + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_disco_bundle_fd_bundle_tile_private_h */ diff --git a/src/disco/plugin/fixtures/2niAy2BYpfqTuBM7pZtTkHFnWM2x7bFcFL1oyR1ixqGRcG5uydjZiG5AR8PRYHAaQ3JqA8JYyCRoc3VCwohQVwYP.bin b/src/disco/bundle/fixtures/2niAy2BYpfqTuBM7pZtTkHFnWM2x7bFcFL1oyR1ixqGRcG5uydjZiG5AR8PRYHAaQ3JqA8JYyCRoc3VCwohQVwYP.bin similarity index 100% rename from src/disco/plugin/fixtures/2niAy2BYpfqTuBM7pZtTkHFnWM2x7bFcFL1oyR1ixqGRcG5uydjZiG5AR8PRYHAaQ3JqA8JYyCRoc3VCwohQVwYP.bin rename to src/disco/bundle/fixtures/2niAy2BYpfqTuBM7pZtTkHFnWM2x7bFcFL1oyR1ixqGRcG5uydjZiG5AR8PRYHAaQ3JqA8JYyCRoc3VCwohQVwYP.bin diff --git a/src/disco/bundle/generated/fd_bundle_tile_seccomp.h b/src/disco/bundle/generated/fd_bundle_tile_seccomp.h new file mode 100644 index 0000000000..704f086c9c --- /dev/null +++ b/src/disco/bundle/generated/fd_bundle_tile_seccomp.h @@ -0,0 +1,126 @@ +/* THIS FILE WAS GENERATED BY generate_filters.py. DO NOT EDIT BY HAND! */ +#ifndef HEADER_fd_src_disco_bundle_generated_fd_bundle_tile_seccomp_h +#define HEADER_fd_src_disco_bundle_generated_fd_bundle_tile_seccomp_h + +#include "../../../../src/util/fd_util_base.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__i386__) +# define ARCH_NR AUDIT_ARCH_I386 +#elif defined(__x86_64__) +# define ARCH_NR AUDIT_ARCH_X86_64 +#elif defined(__aarch64__) +# define ARCH_NR AUDIT_ARCH_AARCH64 +#else +# error "Target architecture is unsupported by seccomp." +#endif +static const unsigned int sock_filter_policy_fd_bundle_tile_instr_cnt = 46; + +static void populate_sock_filter_policy_fd_bundle_tile( ulong out_cnt, struct sock_filter * out, unsigned int logfile_fd) { + FD_TEST( out_cnt >= 46 ); + struct sock_filter filter[46] = { + /* Check: Jump to RET_KILL_PROCESS if the script's arch != the runtime arch */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, ( offsetof( struct seccomp_data, arch ) ) ), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, ARCH_NR, 0, /* RET_KILL_PROCESS */ 42 ), + /* loading syscall number in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, ( offsetof( struct seccomp_data, nr ) ) ), + /* simply allow read */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_read, /* RET_ALLOW */ 41, 0 ), + /* allow recvmsg based on expression */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_recvmsg, /* check_recvmsg */ 13, 0 ), + /* simply allow write */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_write, /* RET_ALLOW */ 39, 0 ), + /* allow sendmsg based on expression */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_sendmsg, /* check_sendmsg */ 13, 0 ), + /* allow fsync based on expression */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_fsync, /* check_fsync */ 14, 0 ), + /* allow socket based on expression */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_socket, /* check_socket */ 15, 0 ), + /* simply allow connect */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_connect, /* RET_ALLOW */ 35, 0 ), + /* allow shutdown based on expression */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_shutdown, /* check_shutdown */ 19, 0 ), + /* simply allow close */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_close, /* RET_ALLOW */ 33, 0 ), + /* allow fcntl based on expression */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_fcntl, /* check_fcntl */ 19, 0 ), + /* allow poll based on expression */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_poll, /* check_poll */ 22, 0 ), + /* allow setsockopt based on expression */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_setsockopt, /* check_setsockopt */ 25, 0 ), + /* simply allow getpid */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_getpid, /* RET_ALLOW */ 29, 0 ), + /* simply allow getrandom */ + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_getrandom, /* RET_ALLOW */ 28, 0 ), + /* none of the syscalls matched */ + { BPF_JMP | BPF_JA, 0, 0, /* RET_KILL_PROCESS */ 26 }, +// check_recvmsg: + /* load syscall argument 2 in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, MSG_NOSIGNAL|MSG_DONTWAIT, /* RET_ALLOW */ 25, /* RET_KILL_PROCESS */ 24 ), +// check_sendmsg: + /* load syscall argument 2 in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, MSG_NOSIGNAL|MSG_DONTWAIT, /* RET_ALLOW */ 23, /* RET_KILL_PROCESS */ 22 ), +// check_fsync: + /* load syscall argument 0 in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, logfile_fd, /* RET_ALLOW */ 21, /* RET_KILL_PROCESS */ 20 ), +// check_socket: + /* load syscall argument 0 in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, AF_INET, /* lbl_1 */ 0, /* RET_KILL_PROCESS */ 18 ), +// lbl_1: + /* load syscall argument 1 in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SOCK_STREAM|SOCK_CLOEXEC, /* lbl_2 */ 0, /* RET_KILL_PROCESS */ 16 ), +// lbl_2: + /* load syscall argument 2 in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_TCP, /* RET_ALLOW */ 15, /* RET_KILL_PROCESS */ 14 ), +// check_shutdown: + /* load syscall argument 1 in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SHUT_WR, /* RET_ALLOW */ 13, /* RET_KILL_PROCESS */ 12 ), +// check_fcntl: + /* load syscall argument 1 in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, F_SETFL, /* lbl_3 */ 0, /* RET_KILL_PROCESS */ 10 ), +// lbl_3: + /* load syscall argument 2 in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, O_NONBLOCK, /* RET_ALLOW */ 9, /* RET_KILL_PROCESS */ 8 ), +// check_poll: + /* load syscall argument 1 in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, 1, /* lbl_4 */ 0, /* RET_KILL_PROCESS */ 6 ), +// lbl_4: + /* load syscall argument 2 in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, 0, /* RET_ALLOW */ 5, /* RET_KILL_PROCESS */ 4 ), +// check_setsockopt: + /* load syscall argument 1 in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SOL_SOCKET, /* lbl_5 */ 0, /* RET_KILL_PROCESS */ 2 ), +// lbl_5: + /* load syscall argument 2 in accumulator */ + BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), + BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SO_RCVBUF, /* RET_ALLOW */ 1, /* RET_KILL_PROCESS */ 0 ), +// RET_KILL_PROCESS: + /* KILL_PROCESS is placed before ALLOW since it's the fallthrough case. */ + BPF_STMT( BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS ), +// RET_ALLOW: + /* ALLOW has to be reached by jumping */ + BPF_STMT( BPF_RET | BPF_K, SECCOMP_RET_ALLOW ), + }; + fd_memcpy( out, filter, sizeof( filter ) ); +} + +#endif diff --git a/src/disco/bundle/proto/Local.mk b/src/disco/bundle/proto/Local.mk new file mode 100644 index 0000000000..7de52eca58 --- /dev/null +++ b/src/disco/bundle/proto/Local.mk @@ -0,0 +1 @@ +$(call add-objs,auth.pb block_engine.pb bundle.pb packet.pb shared.pb timestamp.pb,fd_disco) diff --git a/src/disco/bundle/proto/Makefile b/src/disco/bundle/proto/Makefile new file mode 100644 index 0000000000..27d10e942a --- /dev/null +++ b/src/disco/bundle/proto/Makefile @@ -0,0 +1,20 @@ +NANOPB_GEN?=nanopb_generator.py + +PROTOS:= +PROTOS+=auth +PROTOS+=block_engine +PROTOS+=bundle +PROTOS+=packet +PROTOS+=shared +PROTOS+=timestamp + +.PHONY: generate +generate: $(addsuffix .pb.h,$(PROTOS)) $(addsuffix .pb.c,$(PROTOS)) + +%.pb.h %.pb.c: %.proto %.options + $(NANOPB_GEN) --protoc-opt=--experimental_allow_proto3_optional --options-file $(patsubst %.proto,%.options,$<) $< + sed -i 's|#include |#include "../../../ballet/nanopb/pb_firedancer.h"|' $(patsubst %.proto,%.pb.h,$<) + +.PHONY: clean +clean: + rm -f $(addsuffix .pb.h,$(PROTOS)) $(addsuffix .pb.c,$(PROTOS)) diff --git a/src/disco/bundle/proto/auth.options b/src/disco/bundle/proto/auth.options new file mode 100644 index 0000000000..d904df8f25 --- /dev/null +++ b/src/disco/bundle/proto/auth.options @@ -0,0 +1,11 @@ +auth.GenerateAuthChallengeRequest.pubkey max_size:32 + +auth.GenerateAuthChallengeResponse.challenge max_size:9 + +auth.GenerateAuthTokensRequest.challenge max_size:55 +auth.GenerateAuthTokensRequest.client_pubkey max_size:32 +auth.GenerateAuthTokensRequest.signed_challenge max_size:64 + +auth.RefreshAccessTokenRequest.refresh_token max_size:512 + +auth.Token.value max_size:512 diff --git a/src/disco/bundle/proto/auth.pb.c b/src/disco/bundle/proto/auth.pb.c new file mode 100644 index 0000000000..abb0e4ad54 --- /dev/null +++ b/src/disco/bundle/proto/auth.pb.c @@ -0,0 +1,32 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9.1 */ + +#include "auth.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(auth_GenerateAuthChallengeRequest, auth_GenerateAuthChallengeRequest, AUTO) + + +PB_BIND(auth_GenerateAuthChallengeResponse, auth_GenerateAuthChallengeResponse, AUTO) + + +PB_BIND(auth_GenerateAuthTokensRequest, auth_GenerateAuthTokensRequest, AUTO) + + +PB_BIND(auth_Token, auth_Token, 2) + + +PB_BIND(auth_GenerateAuthTokensResponse, auth_GenerateAuthTokensResponse, 2) + + +PB_BIND(auth_RefreshAccessTokenRequest, auth_RefreshAccessTokenRequest, 2) + + +PB_BIND(auth_RefreshAccessTokenResponse, auth_RefreshAccessTokenResponse, 2) + + + + + diff --git a/src/disco/bundle/proto/auth.pb.h b/src/disco/bundle/proto/auth.pb.h new file mode 100644 index 0000000000..73d3116c84 --- /dev/null +++ b/src/disco/bundle/proto/auth.pb.h @@ -0,0 +1,204 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9.1 */ + +#ifndef PB_AUTH_AUTH_PB_H_INCLUDED +#define PB_AUTH_AUTH_PB_H_INCLUDED +#include "../../../ballet/nanopb/pb_firedancer.h" +#include "timestamp.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +typedef enum _auth_Role { + auth_Role_RELAYER = 0, + auth_Role_SEARCHER = 1, + auth_Role_VALIDATOR = 2, + auth_Role_SHREDSTREAM_SUBSCRIBER = 3 +} auth_Role; + +/* Struct definitions */ +typedef PB_BYTES_ARRAY_T(32) auth_GenerateAuthChallengeRequest_pubkey_t; +typedef struct _auth_GenerateAuthChallengeRequest { + /* / Role the client is attempting to generate tokens for. */ + auth_Role role; + /* / Client's 32 byte pubkey. */ + auth_GenerateAuthChallengeRequest_pubkey_t pubkey; +} auth_GenerateAuthChallengeRequest; + +typedef PB_BYTES_ARRAY_T(9) auth_GenerateAuthChallengeResponse_challenge_t; +typedef struct _auth_GenerateAuthChallengeResponse { + auth_GenerateAuthChallengeResponse_challenge_t challenge; /* string */ +} auth_GenerateAuthChallengeResponse; + +typedef PB_BYTES_ARRAY_T(55) auth_GenerateAuthTokensRequest_challenge_t; +typedef PB_BYTES_ARRAY_T(32) auth_GenerateAuthTokensRequest_client_pubkey_t; +typedef PB_BYTES_ARRAY_T(64) auth_GenerateAuthTokensRequest_signed_challenge_t; +typedef struct _auth_GenerateAuthTokensRequest { + /* / The pre-signed challenge. */ + auth_GenerateAuthTokensRequest_challenge_t challenge; /* string */ + /* / The signing keypair's corresponding 32 byte pubkey. */ + auth_GenerateAuthTokensRequest_client_pubkey_t client_pubkey; + /* / The 64 byte signature of the challenge signed by the client's private key. The private key must correspond to + the pubkey passed in the [GenerateAuthChallenge] method. The client is expected to sign the challenge token + prepended with their pubkey. For example sign(pubkey, challenge). */ + auth_GenerateAuthTokensRequest_signed_challenge_t signed_challenge; +} auth_GenerateAuthTokensRequest; + +typedef PB_BYTES_ARRAY_T(512) auth_Token_value_t; +typedef struct _auth_Token { + /* / The token. */ + auth_Token_value_t value; /* string */ + /* / When the token will expire. */ + bool has_expires_at_utc; + google_protobuf_Timestamp expires_at_utc; +} auth_Token; + +typedef struct _auth_GenerateAuthTokensResponse { + /* / The token granting access to resources. */ + bool has_access_token; + auth_Token access_token; + /* / The token used to refresh the access_token. This has a longer TTL than the access_token. */ + bool has_refresh_token; + auth_Token refresh_token; +} auth_GenerateAuthTokensResponse; + +typedef PB_BYTES_ARRAY_T(512) auth_RefreshAccessTokenRequest_refresh_token_t; +typedef struct _auth_RefreshAccessTokenRequest { + /* / Non-expired refresh token obtained from the [GenerateAuthTokens] method. */ + auth_RefreshAccessTokenRequest_refresh_token_t refresh_token; /* string */ +} auth_RefreshAccessTokenRequest; + +typedef struct _auth_RefreshAccessTokenResponse { + /* / Fresh access_token. */ + bool has_access_token; + auth_Token access_token; +} auth_RefreshAccessTokenResponse; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _auth_Role_MIN auth_Role_RELAYER +#define _auth_Role_MAX auth_Role_SHREDSTREAM_SUBSCRIBER +#define _auth_Role_ARRAYSIZE ((auth_Role)(auth_Role_SHREDSTREAM_SUBSCRIBER+1)) + +#define auth_GenerateAuthChallengeRequest_role_ENUMTYPE auth_Role + + + + + + + + +/* Initializer values for message structs */ +#define auth_GenerateAuthChallengeRequest_init_default {_auth_Role_MIN, {0, {0}}} +#define auth_GenerateAuthChallengeResponse_init_default {{0, {0}}} +#define auth_GenerateAuthTokensRequest_init_default {{0, {0}}, {0, {0}}, {0, {0}}} +#define auth_Token_init_default {{0, {0}}, false, google_protobuf_Timestamp_init_default} +#define auth_GenerateAuthTokensResponse_init_default {false, auth_Token_init_default, false, auth_Token_init_default} +#define auth_RefreshAccessTokenRequest_init_default {{0, {0}}} +#define auth_RefreshAccessTokenResponse_init_default {false, auth_Token_init_default} +#define auth_GenerateAuthChallengeRequest_init_zero {_auth_Role_MIN, {0, {0}}} +#define auth_GenerateAuthChallengeResponse_init_zero {{0, {0}}} +#define auth_GenerateAuthTokensRequest_init_zero {{0, {0}}, {0, {0}}, {0, {0}}} +#define auth_Token_init_zero {{0, {0}}, false, google_protobuf_Timestamp_init_zero} +#define auth_GenerateAuthTokensResponse_init_zero {false, auth_Token_init_zero, false, auth_Token_init_zero} +#define auth_RefreshAccessTokenRequest_init_zero {{0, {0}}} +#define auth_RefreshAccessTokenResponse_init_zero {false, auth_Token_init_zero} + +/* Field tags (for use in manual encoding/decoding) */ +#define auth_GenerateAuthChallengeRequest_role_tag 1 +#define auth_GenerateAuthChallengeRequest_pubkey_tag 2 +#define auth_GenerateAuthChallengeResponse_challenge_tag 1 +#define auth_GenerateAuthTokensRequest_challenge_tag 1 +#define auth_GenerateAuthTokensRequest_client_pubkey_tag 2 +#define auth_GenerateAuthTokensRequest_signed_challenge_tag 3 +#define auth_Token_value_tag 1 +#define auth_Token_expires_at_utc_tag 2 +#define auth_GenerateAuthTokensResponse_access_token_tag 1 +#define auth_GenerateAuthTokensResponse_refresh_token_tag 2 +#define auth_RefreshAccessTokenRequest_refresh_token_tag 1 +#define auth_RefreshAccessTokenResponse_access_token_tag 1 + +/* Struct field encoding specification for nanopb */ +#define auth_GenerateAuthChallengeRequest_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, role, 1) \ +X(a, STATIC, SINGULAR, BYTES, pubkey, 2) +#define auth_GenerateAuthChallengeRequest_CALLBACK NULL +#define auth_GenerateAuthChallengeRequest_DEFAULT NULL + +#define auth_GenerateAuthChallengeResponse_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BYTES, challenge, 1) +#define auth_GenerateAuthChallengeResponse_CALLBACK NULL +#define auth_GenerateAuthChallengeResponse_DEFAULT NULL + +#define auth_GenerateAuthTokensRequest_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BYTES, challenge, 1) \ +X(a, STATIC, SINGULAR, BYTES, client_pubkey, 2) \ +X(a, STATIC, SINGULAR, BYTES, signed_challenge, 3) +#define auth_GenerateAuthTokensRequest_CALLBACK NULL +#define auth_GenerateAuthTokensRequest_DEFAULT NULL + +#define auth_Token_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BYTES, value, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, expires_at_utc, 2) +#define auth_Token_CALLBACK NULL +#define auth_Token_DEFAULT NULL +#define auth_Token_expires_at_utc_MSGTYPE google_protobuf_Timestamp + +#define auth_GenerateAuthTokensResponse_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, access_token, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, refresh_token, 2) +#define auth_GenerateAuthTokensResponse_CALLBACK NULL +#define auth_GenerateAuthTokensResponse_DEFAULT NULL +#define auth_GenerateAuthTokensResponse_access_token_MSGTYPE auth_Token +#define auth_GenerateAuthTokensResponse_refresh_token_MSGTYPE auth_Token + +#define auth_RefreshAccessTokenRequest_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BYTES, refresh_token, 1) +#define auth_RefreshAccessTokenRequest_CALLBACK NULL +#define auth_RefreshAccessTokenRequest_DEFAULT NULL + +#define auth_RefreshAccessTokenResponse_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, access_token, 1) +#define auth_RefreshAccessTokenResponse_CALLBACK NULL +#define auth_RefreshAccessTokenResponse_DEFAULT NULL +#define auth_RefreshAccessTokenResponse_access_token_MSGTYPE auth_Token + +extern const pb_msgdesc_t auth_GenerateAuthChallengeRequest_msg; +extern const pb_msgdesc_t auth_GenerateAuthChallengeResponse_msg; +extern const pb_msgdesc_t auth_GenerateAuthTokensRequest_msg; +extern const pb_msgdesc_t auth_Token_msg; +extern const pb_msgdesc_t auth_GenerateAuthTokensResponse_msg; +extern const pb_msgdesc_t auth_RefreshAccessTokenRequest_msg; +extern const pb_msgdesc_t auth_RefreshAccessTokenResponse_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define auth_GenerateAuthChallengeRequest_fields &auth_GenerateAuthChallengeRequest_msg +#define auth_GenerateAuthChallengeResponse_fields &auth_GenerateAuthChallengeResponse_msg +#define auth_GenerateAuthTokensRequest_fields &auth_GenerateAuthTokensRequest_msg +#define auth_Token_fields &auth_Token_msg +#define auth_GenerateAuthTokensResponse_fields &auth_GenerateAuthTokensResponse_msg +#define auth_RefreshAccessTokenRequest_fields &auth_RefreshAccessTokenRequest_msg +#define auth_RefreshAccessTokenResponse_fields &auth_RefreshAccessTokenResponse_msg + +/* Maximum encoded size of messages (where known) */ +#define AUTH_AUTH_PB_H_MAX_SIZE auth_GenerateAuthTokensResponse_size +#define auth_GenerateAuthChallengeRequest_size 36 +#define auth_GenerateAuthChallengeResponse_size 11 +#define auth_GenerateAuthTokensRequest_size 157 +#define auth_GenerateAuthTokensResponse_size 1084 +#define auth_RefreshAccessTokenRequest_size 515 +#define auth_RefreshAccessTokenResponse_size 542 +#define auth_Token_size 539 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/disco/bundle/proto/auth.proto b/src/disco/bundle/proto/auth.proto new file mode 100644 index 0000000000..ab85be6b9c --- /dev/null +++ b/src/disco/bundle/proto/auth.proto @@ -0,0 +1,76 @@ +syntax = "proto3"; + +package auth; + +import "timestamp.proto"; + +enum Role { + RELAYER = 0; + SEARCHER = 1; + VALIDATOR = 2; + SHREDSTREAM_SUBSCRIBER = 3; +} + +message GenerateAuthChallengeRequest { + /// Role the client is attempting to generate tokens for. + Role role = 1; + + /// Client's 32 byte pubkey. + bytes pubkey = 2; +} + +message GenerateAuthChallengeResponse { + bytes challenge = 1; // string +} + +message GenerateAuthTokensRequest { + /// The pre-signed challenge. + bytes challenge = 1; // string + + /// The signing keypair's corresponding 32 byte pubkey. + bytes client_pubkey = 2; + + /// The 64 byte signature of the challenge signed by the client's private key. The private key must correspond to + // the pubkey passed in the [GenerateAuthChallenge] method. The client is expected to sign the challenge token + // prepended with their pubkey. For example sign(pubkey, challenge). + bytes signed_challenge = 3; +} + +message Token { + /// The token. + bytes value = 1; // string + + /// When the token will expire. + google.protobuf.Timestamp expires_at_utc = 2; +} + +message GenerateAuthTokensResponse { + /// The token granting access to resources. + Token access_token = 1; + + /// The token used to refresh the access_token. This has a longer TTL than the access_token. + Token refresh_token = 2; +} + +message RefreshAccessTokenRequest { + /// Non-expired refresh token obtained from the [GenerateAuthTokens] method. + bytes refresh_token = 1; // string +} + +message RefreshAccessTokenResponse { + /// Fresh access_token. + Token access_token = 1; +} + +/// This service is responsible for issuing auth tokens to clients for API access. +service AuthService { + /// Returns a challenge, client is expected to sign this challenge with an appropriate keypair in order to obtain access tokens. + rpc GenerateAuthChallenge(GenerateAuthChallengeRequest) returns (GenerateAuthChallengeResponse) {} + + /// Provides the client with the initial pair of auth tokens for API access. + rpc GenerateAuthTokens(GenerateAuthTokensRequest) returns (GenerateAuthTokensResponse) {} + + /// Call this method with a non-expired refresh token to obtain a new access token. + rpc RefreshAccessToken(RefreshAccessTokenRequest) returns (RefreshAccessTokenResponse) {} +} + diff --git a/src/disco/bundle/proto/block_engine.options b/src/disco/bundle/proto/block_engine.options new file mode 100644 index 0000000000..bc0af79fff --- /dev/null +++ b/src/disco/bundle/proto/block_engine.options @@ -0,0 +1 @@ +block_engine.BlockBuilderFeeInfoResponse.pubkey max_length:44 diff --git a/src/disco/bundle/proto/block_engine.pb.c b/src/disco/bundle/proto/block_engine.pb.c new file mode 100644 index 0000000000..15b49d3a19 --- /dev/null +++ b/src/disco/bundle/proto/block_engine.pb.c @@ -0,0 +1,27 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9.1 */ + +#include "block_engine.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(block_engine_SubscribePacketsRequest, block_engine_SubscribePacketsRequest, AUTO) + + +PB_BIND(block_engine_SubscribePacketsResponse, block_engine_SubscribePacketsResponse, AUTO) + + +PB_BIND(block_engine_SubscribeBundlesRequest, block_engine_SubscribeBundlesRequest, AUTO) + + +PB_BIND(block_engine_SubscribeBundlesResponse, block_engine_SubscribeBundlesResponse, AUTO) + + +PB_BIND(block_engine_BlockBuilderFeeInfoRequest, block_engine_BlockBuilderFeeInfoRequest, AUTO) + + +PB_BIND(block_engine_BlockBuilderFeeInfoResponse, block_engine_BlockBuilderFeeInfoResponse, AUTO) + + + diff --git a/src/disco/bundle/proto/block_engine.pb.h b/src/disco/bundle/proto/block_engine.pb.h new file mode 100644 index 0000000000..550975e674 --- /dev/null +++ b/src/disco/bundle/proto/block_engine.pb.h @@ -0,0 +1,137 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9.1 */ + +#ifndef PB_BLOCK_ENGINE_BLOCK_ENGINE_PB_H_INCLUDED +#define PB_BLOCK_ENGINE_BLOCK_ENGINE_PB_H_INCLUDED +#include "../../../ballet/nanopb/pb_firedancer.h" +#include "packet.pb.h" +#include "shared.pb.h" +#include "bundle.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +typedef struct _block_engine_SubscribePacketsRequest { + char dummy_field; +} block_engine_SubscribePacketsRequest; + +typedef struct _block_engine_SubscribePacketsResponse { + bool has_header; + shared_Header header; + bool has_batch; + packet_PacketBatch batch; +} block_engine_SubscribePacketsResponse; + +typedef struct _block_engine_SubscribeBundlesRequest { + char dummy_field; +} block_engine_SubscribeBundlesRequest; + +typedef struct _block_engine_SubscribeBundlesResponse { + pb_callback_t bundles; +} block_engine_SubscribeBundlesResponse; + +typedef struct _block_engine_BlockBuilderFeeInfoRequest { + char dummy_field; +} block_engine_BlockBuilderFeeInfoRequest; + +typedef struct _block_engine_BlockBuilderFeeInfoResponse { + char pubkey[45]; + /* commission (0-100) */ + uint64_t commission; +} block_engine_BlockBuilderFeeInfoResponse; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define block_engine_SubscribePacketsRequest_init_default {0} +#define block_engine_SubscribePacketsResponse_init_default {false, shared_Header_init_default, false, packet_PacketBatch_init_default} +#define block_engine_SubscribeBundlesRequest_init_default {0} +#define block_engine_SubscribeBundlesResponse_init_default {{{NULL}, NULL}} +#define block_engine_BlockBuilderFeeInfoRequest_init_default {0} +#define block_engine_BlockBuilderFeeInfoResponse_init_default {"", 0} +#define block_engine_SubscribePacketsRequest_init_zero {0} +#define block_engine_SubscribePacketsResponse_init_zero {false, shared_Header_init_zero, false, packet_PacketBatch_init_zero} +#define block_engine_SubscribeBundlesRequest_init_zero {0} +#define block_engine_SubscribeBundlesResponse_init_zero {{{NULL}, NULL}} +#define block_engine_BlockBuilderFeeInfoRequest_init_zero {0} +#define block_engine_BlockBuilderFeeInfoResponse_init_zero {"", 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define block_engine_SubscribePacketsResponse_header_tag 1 +#define block_engine_SubscribePacketsResponse_batch_tag 2 +#define block_engine_SubscribeBundlesResponse_bundles_tag 1 +#define block_engine_BlockBuilderFeeInfoResponse_pubkey_tag 1 +#define block_engine_BlockBuilderFeeInfoResponse_commission_tag 2 + +/* Struct field encoding specification for nanopb */ +#define block_engine_SubscribePacketsRequest_FIELDLIST(X, a) \ + +#define block_engine_SubscribePacketsRequest_CALLBACK NULL +#define block_engine_SubscribePacketsRequest_DEFAULT NULL + +#define block_engine_SubscribePacketsResponse_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, header, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, batch, 2) +#define block_engine_SubscribePacketsResponse_CALLBACK NULL +#define block_engine_SubscribePacketsResponse_DEFAULT NULL +#define block_engine_SubscribePacketsResponse_header_MSGTYPE shared_Header +#define block_engine_SubscribePacketsResponse_batch_MSGTYPE packet_PacketBatch + +#define block_engine_SubscribeBundlesRequest_FIELDLIST(X, a) \ + +#define block_engine_SubscribeBundlesRequest_CALLBACK NULL +#define block_engine_SubscribeBundlesRequest_DEFAULT NULL + +#define block_engine_SubscribeBundlesResponse_FIELDLIST(X, a) \ +X(a, CALLBACK, REPEATED, MESSAGE, bundles, 1) +#define block_engine_SubscribeBundlesResponse_CALLBACK pb_default_field_callback +#define block_engine_SubscribeBundlesResponse_DEFAULT NULL +#define block_engine_SubscribeBundlesResponse_bundles_MSGTYPE bundle_BundleUuid + +#define block_engine_BlockBuilderFeeInfoRequest_FIELDLIST(X, a) \ + +#define block_engine_BlockBuilderFeeInfoRequest_CALLBACK NULL +#define block_engine_BlockBuilderFeeInfoRequest_DEFAULT NULL + +#define block_engine_BlockBuilderFeeInfoResponse_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, pubkey, 1) \ +X(a, STATIC, SINGULAR, UINT64, commission, 2) +#define block_engine_BlockBuilderFeeInfoResponse_CALLBACK NULL +#define block_engine_BlockBuilderFeeInfoResponse_DEFAULT NULL + +extern const pb_msgdesc_t block_engine_SubscribePacketsRequest_msg; +extern const pb_msgdesc_t block_engine_SubscribePacketsResponse_msg; +extern const pb_msgdesc_t block_engine_SubscribeBundlesRequest_msg; +extern const pb_msgdesc_t block_engine_SubscribeBundlesResponse_msg; +extern const pb_msgdesc_t block_engine_BlockBuilderFeeInfoRequest_msg; +extern const pb_msgdesc_t block_engine_BlockBuilderFeeInfoResponse_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define block_engine_SubscribePacketsRequest_fields &block_engine_SubscribePacketsRequest_msg +#define block_engine_SubscribePacketsResponse_fields &block_engine_SubscribePacketsResponse_msg +#define block_engine_SubscribeBundlesRequest_fields &block_engine_SubscribeBundlesRequest_msg +#define block_engine_SubscribeBundlesResponse_fields &block_engine_SubscribeBundlesResponse_msg +#define block_engine_BlockBuilderFeeInfoRequest_fields &block_engine_BlockBuilderFeeInfoRequest_msg +#define block_engine_BlockBuilderFeeInfoResponse_fields &block_engine_BlockBuilderFeeInfoResponse_msg + +/* Maximum encoded size of messages (where known) */ +/* block_engine_SubscribeBundlesResponse_size depends on runtime parameters */ +#define BLOCK_ENGINE_BLOCK_ENGINE_PB_H_MAX_SIZE block_engine_BlockBuilderFeeInfoResponse_size +#define block_engine_BlockBuilderFeeInfoRequest_size 0 +#define block_engine_BlockBuilderFeeInfoResponse_size 57 +#define block_engine_SubscribeBundlesRequest_size 0 +#define block_engine_SubscribePacketsRequest_size 0 +#if defined(packet_PacketBatch_size) +#define block_engine_SubscribePacketsResponse_size (32 + packet_PacketBatch_size) +#endif + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/disco/bundle/proto/block_engine.proto b/src/disco/bundle/proto/block_engine.proto new file mode 100644 index 0000000000..20123106c9 --- /dev/null +++ b/src/disco/bundle/proto/block_engine.proto @@ -0,0 +1,39 @@ +syntax = "proto3"; + +import "packet.proto"; +import "shared.proto"; +import "bundle.proto"; + +package block_engine; + +message SubscribePacketsRequest {} +message SubscribePacketsResponse { + shared.Header header = 1; + packet.PacketBatch batch = 2; +} + +message SubscribeBundlesRequest {} +message SubscribeBundlesResponse { + repeated bundle.BundleUuid bundles = 1; +} + +message BlockBuilderFeeInfoRequest {} +message BlockBuilderFeeInfoResponse { + string pubkey = 1; + + // commission (0-100) + uint64 commission = 2; +} + +/// Validators can connect to Block Engines to receive packets and bundles. +service BlockEngineValidator { + /// Validators can subscribe to the block engine to receive a stream of packets + rpc SubscribePackets (SubscribePacketsRequest) returns (stream SubscribePacketsResponse) {} + + /// Validators can subscribe to the block engine to receive a stream of simulated and profitable bundles + rpc SubscribeBundles (SubscribeBundlesRequest) returns (stream SubscribeBundlesResponse) {} + + // Block builders can optionally collect fees. This returns fee information if a block builder wants to + // collect one. + rpc GetBlockBuilderFeeInfo (BlockBuilderFeeInfoRequest) returns (BlockBuilderFeeInfoResponse) {} +} diff --git a/src/disco/bundle/proto/bundle.options b/src/disco/bundle/proto/bundle.options new file mode 100644 index 0000000000..5864747055 --- /dev/null +++ b/src/disco/bundle/proto/bundle.options @@ -0,0 +1 @@ +bundle.BundleUuid.uuid max_size:128 diff --git a/src/disco/bundle/proto/bundle.pb.c b/src/disco/bundle/proto/bundle.pb.c new file mode 100644 index 0000000000..64c3493ecc --- /dev/null +++ b/src/disco/bundle/proto/bundle.pb.c @@ -0,0 +1,15 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9.1 */ + +#include "bundle.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(bundle_Bundle, bundle_Bundle, AUTO) + + +PB_BIND(bundle_BundleUuid, bundle_BundleUuid, AUTO) + + + diff --git a/src/disco/bundle/proto/bundle.pb.h b/src/disco/bundle/proto/bundle.pb.h new file mode 100644 index 0000000000..4005e5b2d2 --- /dev/null +++ b/src/disco/bundle/proto/bundle.pb.h @@ -0,0 +1,76 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9.1 */ + +#ifndef PB_BUNDLE_BUNDLE_PB_H_INCLUDED +#define PB_BUNDLE_BUNDLE_PB_H_INCLUDED +#include "../../../ballet/nanopb/pb_firedancer.h" +#include "packet.pb.h" +#include "shared.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +typedef struct _bundle_Bundle { + bool has_header; + shared_Header header; + pb_callback_t packets; +} bundle_Bundle; + +typedef PB_BYTES_ARRAY_T(128) bundle_BundleUuid_uuid_t; +typedef struct _bundle_BundleUuid { + bool has_bundle; + bundle_Bundle bundle; + bundle_BundleUuid_uuid_t uuid; +} bundle_BundleUuid; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define bundle_Bundle_init_default {false, shared_Header_init_default, {{NULL}, NULL}} +#define bundle_BundleUuid_init_default {false, bundle_Bundle_init_default, {0, {0}}} +#define bundle_Bundle_init_zero {false, shared_Header_init_zero, {{NULL}, NULL}} +#define bundle_BundleUuid_init_zero {false, bundle_Bundle_init_zero, {0, {0}}} + +/* Field tags (for use in manual encoding/decoding) */ +#define bundle_Bundle_header_tag 2 +#define bundle_Bundle_packets_tag 3 +#define bundle_BundleUuid_bundle_tag 1 +#define bundle_BundleUuid_uuid_tag 2 + +/* Struct field encoding specification for nanopb */ +#define bundle_Bundle_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, header, 2) \ +X(a, CALLBACK, REPEATED, MESSAGE, packets, 3) +#define bundle_Bundle_CALLBACK pb_default_field_callback +#define bundle_Bundle_DEFAULT NULL +#define bundle_Bundle_header_MSGTYPE shared_Header +#define bundle_Bundle_packets_MSGTYPE packet_Packet + +#define bundle_BundleUuid_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, bundle, 1) \ +X(a, STATIC, SINGULAR, BYTES, uuid, 2) +#define bundle_BundleUuid_CALLBACK NULL +#define bundle_BundleUuid_DEFAULT NULL +#define bundle_BundleUuid_bundle_MSGTYPE bundle_Bundle + +extern const pb_msgdesc_t bundle_Bundle_msg; +extern const pb_msgdesc_t bundle_BundleUuid_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define bundle_Bundle_fields &bundle_Bundle_msg +#define bundle_BundleUuid_fields &bundle_BundleUuid_msg + +/* Maximum encoded size of messages (where known) */ +/* bundle_Bundle_size depends on runtime parameters */ +/* bundle_BundleUuid_size depends on runtime parameters */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/disco/bundle/proto/bundle.proto b/src/disco/bundle/proto/bundle.proto new file mode 100644 index 0000000000..7f0903a29c --- /dev/null +++ b/src/disco/bundle/proto/bundle.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +import "packet.proto"; +import "shared.proto"; + +package bundle; + +message Bundle { + shared.Header header = 2; + repeated packet.Packet packets = 3; +} + +message BundleUuid { + bundle.Bundle bundle = 1; + bytes uuid = 2; +} diff --git a/src/disco/bundle/proto/packet.options b/src/disco/bundle/proto/packet.options new file mode 100644 index 0000000000..3ccb8a075e --- /dev/null +++ b/src/disco/bundle/proto/packet.options @@ -0,0 +1,2 @@ +packet.Meta.addr max_size:256 +packet.Packet.data max_size:2048 diff --git a/src/disco/bundle/proto/packet.pb.c b/src/disco/bundle/proto/packet.pb.c new file mode 100644 index 0000000000..f4f05aaf22 --- /dev/null +++ b/src/disco/bundle/proto/packet.pb.c @@ -0,0 +1,21 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9.1 */ + +#include "packet.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(packet_PacketBatch, packet_PacketBatch, AUTO) + + +PB_BIND(packet_Packet, packet_Packet, 2) + + +PB_BIND(packet_Meta, packet_Meta, 2) + + +PB_BIND(packet_PacketFlags, packet_PacketFlags, AUTO) + + + diff --git a/src/disco/bundle/proto/packet.pb.h b/src/disco/bundle/proto/packet.pb.h new file mode 100644 index 0000000000..f7b8008a28 --- /dev/null +++ b/src/disco/bundle/proto/packet.pb.h @@ -0,0 +1,129 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9.1 */ + +#ifndef PB_PACKET_PACKET_PB_H_INCLUDED +#define PB_PACKET_PACKET_PB_H_INCLUDED +#include "../../../ballet/nanopb/pb_firedancer.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +typedef struct _packet_PacketBatch { + pb_callback_t packets; +} packet_PacketBatch; + +typedef struct _packet_PacketFlags { + bool discard; + bool forwarded; + bool repair; + bool simple_vote_tx; + bool tracer_packet; + bool from_staked_node; +} packet_PacketFlags; + +typedef struct _packet_Meta { + uint64_t size; + char addr[256]; + uint32_t port; + bool has_flags; + packet_PacketFlags flags; + uint64_t sender_stake; +} packet_Meta; + +typedef PB_BYTES_ARRAY_T(2048) packet_Packet_data_t; +typedef struct _packet_Packet { + packet_Packet_data_t data; + bool has_meta; + packet_Meta meta; +} packet_Packet; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define packet_PacketBatch_init_default {{{NULL}, NULL}} +#define packet_Packet_init_default {{0, {0}}, false, packet_Meta_init_default} +#define packet_Meta_init_default {0, "", 0, false, packet_PacketFlags_init_default, 0} +#define packet_PacketFlags_init_default {0, 0, 0, 0, 0, 0} +#define packet_PacketBatch_init_zero {{{NULL}, NULL}} +#define packet_Packet_init_zero {{0, {0}}, false, packet_Meta_init_zero} +#define packet_Meta_init_zero {0, "", 0, false, packet_PacketFlags_init_zero, 0} +#define packet_PacketFlags_init_zero {0, 0, 0, 0, 0, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define packet_PacketBatch_packets_tag 1 +#define packet_PacketFlags_discard_tag 1 +#define packet_PacketFlags_forwarded_tag 2 +#define packet_PacketFlags_repair_tag 3 +#define packet_PacketFlags_simple_vote_tx_tag 4 +#define packet_PacketFlags_tracer_packet_tag 5 +#define packet_PacketFlags_from_staked_node_tag 6 +#define packet_Meta_size_tag 1 +#define packet_Meta_addr_tag 2 +#define packet_Meta_port_tag 3 +#define packet_Meta_flags_tag 4 +#define packet_Meta_sender_stake_tag 5 +#define packet_Packet_data_tag 1 +#define packet_Packet_meta_tag 2 + +/* Struct field encoding specification for nanopb */ +#define packet_PacketBatch_FIELDLIST(X, a) \ +X(a, CALLBACK, REPEATED, MESSAGE, packets, 1) +#define packet_PacketBatch_CALLBACK pb_default_field_callback +#define packet_PacketBatch_DEFAULT NULL +#define packet_PacketBatch_packets_MSGTYPE packet_Packet + +#define packet_Packet_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BYTES, data, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, meta, 2) +#define packet_Packet_CALLBACK NULL +#define packet_Packet_DEFAULT NULL +#define packet_Packet_meta_MSGTYPE packet_Meta + +#define packet_Meta_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT64, size, 1) \ +X(a, STATIC, SINGULAR, STRING, addr, 2) \ +X(a, STATIC, SINGULAR, UINT32, port, 3) \ +X(a, STATIC, OPTIONAL, MESSAGE, flags, 4) \ +X(a, STATIC, SINGULAR, UINT64, sender_stake, 5) +#define packet_Meta_CALLBACK NULL +#define packet_Meta_DEFAULT NULL +#define packet_Meta_flags_MSGTYPE packet_PacketFlags + +#define packet_PacketFlags_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, discard, 1) \ +X(a, STATIC, SINGULAR, BOOL, forwarded, 2) \ +X(a, STATIC, SINGULAR, BOOL, repair, 3) \ +X(a, STATIC, SINGULAR, BOOL, simple_vote_tx, 4) \ +X(a, STATIC, SINGULAR, BOOL, tracer_packet, 5) \ +X(a, STATIC, SINGULAR, BOOL, from_staked_node, 6) +#define packet_PacketFlags_CALLBACK NULL +#define packet_PacketFlags_DEFAULT NULL + +extern const pb_msgdesc_t packet_PacketBatch_msg; +extern const pb_msgdesc_t packet_Packet_msg; +extern const pb_msgdesc_t packet_Meta_msg; +extern const pb_msgdesc_t packet_PacketFlags_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define packet_PacketBatch_fields &packet_PacketBatch_msg +#define packet_Packet_fields &packet_Packet_msg +#define packet_Meta_fields &packet_Meta_msg +#define packet_PacketFlags_fields &packet_PacketFlags_msg + +/* Maximum encoded size of messages (where known) */ +/* packet_PacketBatch_size depends on runtime parameters */ +#define PACKET_PACKET_PB_H_MAX_SIZE packet_Packet_size +#define packet_Meta_size 300 +#define packet_PacketFlags_size 12 +#define packet_Packet_size 2354 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/disco/bundle/proto/packet.proto b/src/disco/bundle/proto/packet.proto new file mode 100644 index 0000000000..23042a3c64 --- /dev/null +++ b/src/disco/bundle/proto/packet.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package packet; + +message PacketBatch { + repeated Packet packets = 1; +} + +message Packet { + bytes data = 1; + Meta meta = 2; +} + +message Meta { + uint64 size = 1; + string addr = 2; + uint32 port = 3; + PacketFlags flags = 4; + uint64 sender_stake = 5; +} + +message PacketFlags { + bool discard = 1; + bool forwarded = 2; + bool repair = 3; + bool simple_vote_tx = 4; + bool tracer_packet = 5; + bool from_staked_node = 6; +} + diff --git a/src/disco/bundle/proto/shared.options b/src/disco/bundle/proto/shared.options new file mode 100644 index 0000000000..042f5ef65b --- /dev/null +++ b/src/disco/bundle/proto/shared.options @@ -0,0 +1 @@ +shared.Socket.ip max_size:64 diff --git a/src/disco/bundle/proto/shared.pb.c b/src/disco/bundle/proto/shared.pb.c new file mode 100644 index 0000000000..4f7897e8bc --- /dev/null +++ b/src/disco/bundle/proto/shared.pb.c @@ -0,0 +1,18 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9.1 */ + +#include "shared.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(shared_Header, shared_Header, AUTO) + + +PB_BIND(shared_Heartbeat, shared_Heartbeat, AUTO) + + +PB_BIND(shared_Socket, shared_Socket, AUTO) + + + diff --git a/src/disco/bundle/proto/shared.pb.h b/src/disco/bundle/proto/shared.pb.h new file mode 100644 index 0000000000..2f91ad78d8 --- /dev/null +++ b/src/disco/bundle/proto/shared.pb.h @@ -0,0 +1,85 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9.1 */ + +#ifndef PB_SHARED_SHARED_PB_H_INCLUDED +#define PB_SHARED_SHARED_PB_H_INCLUDED +#include "../../../ballet/nanopb/pb_firedancer.h" +#include "timestamp.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +typedef struct _shared_Header { + bool has_ts; + google_protobuf_Timestamp ts; +} shared_Header; + +typedef struct _shared_Heartbeat { + uint64_t count; +} shared_Heartbeat; + +typedef PB_BYTES_ARRAY_T(64) shared_Socket_ip_t; +typedef struct _shared_Socket { + shared_Socket_ip_t ip; /* string */ + int64_t port; +} shared_Socket; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define shared_Header_init_default {false, google_protobuf_Timestamp_init_default} +#define shared_Heartbeat_init_default {0} +#define shared_Socket_init_default {{0, {0}}, 0} +#define shared_Header_init_zero {false, google_protobuf_Timestamp_init_zero} +#define shared_Heartbeat_init_zero {0} +#define shared_Socket_init_zero {{0, {0}}, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define shared_Header_ts_tag 1 +#define shared_Heartbeat_count_tag 1 +#define shared_Socket_ip_tag 1 +#define shared_Socket_port_tag 2 + +/* Struct field encoding specification for nanopb */ +#define shared_Header_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, ts, 1) +#define shared_Header_CALLBACK NULL +#define shared_Header_DEFAULT NULL +#define shared_Header_ts_MSGTYPE google_protobuf_Timestamp + +#define shared_Heartbeat_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT64, count, 1) +#define shared_Heartbeat_CALLBACK NULL +#define shared_Heartbeat_DEFAULT NULL + +#define shared_Socket_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BYTES, ip, 1) \ +X(a, STATIC, SINGULAR, INT64, port, 2) +#define shared_Socket_CALLBACK NULL +#define shared_Socket_DEFAULT NULL + +extern const pb_msgdesc_t shared_Header_msg; +extern const pb_msgdesc_t shared_Heartbeat_msg; +extern const pb_msgdesc_t shared_Socket_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define shared_Header_fields &shared_Header_msg +#define shared_Heartbeat_fields &shared_Heartbeat_msg +#define shared_Socket_fields &shared_Socket_msg + +/* Maximum encoded size of messages (where known) */ +#define SHARED_SHARED_PB_H_MAX_SIZE shared_Socket_size +#define shared_Header_size 24 +#define shared_Heartbeat_size 11 +#define shared_Socket_size 77 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/disco/bundle/proto/shared.proto b/src/disco/bundle/proto/shared.proto new file mode 100644 index 0000000000..88333d5b62 --- /dev/null +++ b/src/disco/bundle/proto/shared.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +import "timestamp.proto"; + +package shared; + +message Header { + google.protobuf.Timestamp ts = 1; +} + +message Heartbeat { + uint64 count = 1; +} + +message Socket { + bytes ip = 1; // string + int64 port = 2; +} diff --git a/src/disco/bundle/proto/timestamp.options b/src/disco/bundle/proto/timestamp.options new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/disco/bundle/proto/timestamp.pb.c b/src/disco/bundle/proto/timestamp.pb.c new file mode 100644 index 0000000000..61a90b2bea --- /dev/null +++ b/src/disco/bundle/proto/timestamp.pb.c @@ -0,0 +1,12 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9.1 */ + +#include "timestamp.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(google_protobuf_Timestamp, google_protobuf_Timestamp, AUTO) + + + diff --git a/src/disco/bundle/proto/timestamp.pb.h b/src/disco/bundle/proto/timestamp.pb.h new file mode 100644 index 0000000000..fe2f324144 --- /dev/null +++ b/src/disco/bundle/proto/timestamp.pb.h @@ -0,0 +1,51 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9.1 */ + +#ifndef PB_GOOGLE_PROTOBUF_TIMESTAMP_PB_H_INCLUDED +#define PB_GOOGLE_PROTOBUF_TIMESTAMP_PB_H_INCLUDED +#include "../../../ballet/nanopb/pb_firedancer.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +typedef struct _google_protobuf_Timestamp { + int64_t seconds; + int32_t nanos; +} google_protobuf_Timestamp; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define google_protobuf_Timestamp_init_default {0, 0} +#define google_protobuf_Timestamp_init_zero {0, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define google_protobuf_Timestamp_seconds_tag 1 +#define google_protobuf_Timestamp_nanos_tag 2 + +/* Struct field encoding specification for nanopb */ +#define google_protobuf_Timestamp_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, INT64, seconds, 1) \ +X(a, STATIC, SINGULAR, INT32, nanos, 2) +#define google_protobuf_Timestamp_CALLBACK NULL +#define google_protobuf_Timestamp_DEFAULT NULL + +extern const pb_msgdesc_t google_protobuf_Timestamp_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define google_protobuf_Timestamp_fields &google_protobuf_Timestamp_msg + +/* Maximum encoded size of messages (where known) */ +#define GOOGLE_PROTOBUF_TIMESTAMP_PB_H_MAX_SIZE google_protobuf_Timestamp_size +#define google_protobuf_Timestamp_size 22 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/disco/bundle/proto/timestamp.proto b/src/disco/bundle/proto/timestamp.proto new file mode 100644 index 0000000000..6f19482bf0 --- /dev/null +++ b/src/disco/bundle/proto/timestamp.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package google.protobuf; + +message Timestamp { + int64 seconds = 1; + int32 nanos = 2; +} diff --git a/src/disco/bundle/test_bundle_client.c b/src/disco/bundle/test_bundle_client.c new file mode 100644 index 0000000000..bf265099da --- /dev/null +++ b/src/disco/bundle/test_bundle_client.c @@ -0,0 +1,175 @@ +#include "fd_bundle_tile_private.h" +#include "../tiles.h" /* FD_TPU_PARSED_MTU */ + +FD_IMPORT_BINARY( test_bundle_response, "src/disco/bundle/test_bundle_response.binpb" ); + +__attribute__((weak)) char const fdctl_version_string[] = "0.0.0"; + +/* Util for creating a mock bundle topology. */ + +struct test_bundle_env { + fd_stem_context_t stem[1]; + ulong stem_seqs [1]; + ulong stem_depths [1]; + ulong stem_cr_avail[1]; + fd_frag_meta_t * out_mcache; + uchar * out_dcache; + + fd_bundle_tile_t state[1]; +}; + +typedef struct test_bundle_env test_bundle_env_t; + +static test_bundle_env_t * +test_bundle_env_create( test_bundle_env_t * env, + fd_wksp_t * wksp ) { + fd_memset( env, 0, sizeof(test_bundle_env_t) ); + + ulong const mcache_depth = FD_MCACHE_BLOCK; + fd_frag_meta_t * mcache = fd_mcache_join( fd_mcache_new( + fd_wksp_alloc_laddr( wksp, fd_mcache_align(), fd_mcache_footprint( mcache_depth, 0UL ), 1UL ), + FD_MCACHE_BLOCK, 0UL, 0UL ) ); + FD_TEST( mcache ); + + ulong const mtu = FD_TPU_PARSED_MTU; + ulong const dcache_data_sz = fd_dcache_req_data_sz( mtu, mcache_depth, 1UL, 1 ); + void * dcache = fd_dcache_join( fd_dcache_new( + fd_wksp_alloc_laddr( wksp, fd_dcache_align(), fd_dcache_footprint( dcache_data_sz, 0UL ), 1UL ), + dcache_data_sz, 0UL ) ); + FD_TEST( dcache ); + + /* Create a fake stem context */ + env->out_mcache = mcache; + env->out_dcache = dcache; + env->stem_seqs [0] = 0UL; + env->stem_depths [0] = mcache_depth; + env->stem_cr_avail[0] = ULONG_MAX; + *env->stem = (fd_stem_context_t) { + .mcaches = &env->out_mcache, + .seqs = env->stem_seqs, + .depths = env->stem_depths, + .cr_avail = env->stem_cr_avail, + .cr_decrement_amount = 0UL + }; + + fd_bundle_tile_t * state = env->state; + state->stem = env->stem; + state->verify_out = (fd_bundle_out_ctx_t) { + .mem = dcache, + .chunk0 = 0UL, + .chunk = 0UL, + .wmark = fd_dcache_compact_wmark( dcache, dcache, FD_TPU_PARSED_MTU ), + .idx = 0UL, + }; + return env; +} + +static void +test_bundle_env_destroy( test_bundle_env_t * env ) { + fd_wksp_free_laddr( fd_mcache_delete( fd_mcache_leave( env->out_mcache ) ) ); + fd_wksp_free_laddr( fd_dcache_delete( fd_dcache_leave( env->out_dcache ) ) ); + fd_memset( env, 0, sizeof(test_bundle_env_t) ); +} + +/* Test that packets and bundles get forwarded correctly to Firedancer + components. */ + +static void +test_data_path( fd_wksp_t * wksp ) { + test_bundle_env_t env[1]; test_bundle_env_create( env, wksp ); + fd_bundle_tile_t * state = env->state; + + static uchar subscribe_packets_msg[] = { + 0x12, 0x13, 0x0a, 0x07, 0x0a, 0x01, 0x48, 0x12, + 0x02, 0x08, 0x01, 0x0a, 0x08, 0x0a, 0x02, 0x48, + 0x48, 0x12, 0x02, 0x08, 0x02 + }; + fd_bundle_client_grpc_rx_msg( + state, + subscribe_packets_msg, sizeof(subscribe_packets_msg), + FD_BUNDLE_CLIENT_REQ_Bundle_SubscribePackets + ); + + /* Wipe timestamps */ + for( ulong i=0UL; i<(env->stem_depths[0]); i++ ) { + env->out_mcache[ i ].tsorig = 0U; + env->out_mcache[ i ].tspub = 0U; + } + + fd_frag_meta_t expected[2] = { + { .seq=0UL, .sig=0UL, .chunk=0, .sz=sizeof(fd_txn_m_t)+8, .ctl=0 }, + { .seq=1UL, .sig=0UL, .chunk=2, .sz=sizeof(fd_txn_m_t)+8, .ctl=0 } + }; + FD_TEST( fd_memeq( env->out_mcache, expected, 2*sizeof(fd_frag_meta_t) ) ); + + state->builder_info_avail = 1; + + fd_bundle_client_grpc_rx_msg( + state, + test_bundle_response, test_bundle_response_sz, + FD_BUNDLE_CLIENT_REQ_Bundle_SubscribeBundles + ); + + test_bundle_env_destroy( env ); +} + +/* Ensure forwarding of bundles stops when builder fee info is missing. */ + +static void +test_missing_builder_fee_info( fd_wksp_t * wksp ) { + test_bundle_env_t env[1]; test_bundle_env_create( env, wksp ); + fd_bundle_tile_t * state = env->state; + state->builder_info_avail = 0; + + /* Regular packets are always forwarded */ + static uchar subscribe_packets_msg[] = { + 0x12, 0x09, 0x0a, 0x07, 0x0a, 0x01, 0x48, 0x12, + 0x02, 0x08, 0x01 + }; + fd_bundle_client_grpc_rx_msg( + state, + subscribe_packets_msg, sizeof(subscribe_packets_msg), + FD_BUNDLE_CLIENT_REQ_Bundle_SubscribePackets + ); + FD_TEST( fd_seq_eq( env->out_mcache[ 0 ].seq, 0UL ) ); + FD_TEST( state->metrics.packet_received_cnt ==1UL ); + FD_TEST( state->metrics.missing_builder_info_fail_cnt==0UL ); + + /* Bundles are no longer forwarded */ + + fd_bundle_client_grpc_rx_msg( + state, + test_bundle_response, test_bundle_response_sz, + FD_BUNDLE_CLIENT_REQ_Bundle_SubscribeBundles + ); + FD_TEST( fd_seq_ne( env->out_mcache[ 1 ].seq, 1UL ) ); + FD_TEST( state->metrics.bundle_received_cnt ==0UL ); + FD_TEST( state->metrics.missing_builder_info_fail_cnt==1UL ); + + test_bundle_env_destroy( env ); +} + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + + ulong cpu_idx = fd_tile_cpu_id( fd_tile_idx() ); + if( cpu_idx>fd_shmem_cpu_cnt() ) cpu_idx = 0UL; + + char const * _page_sz = fd_env_strip_cmdline_cstr ( &argc, &argv, "--page-sz", NULL, "normal" ); + ulong page_cnt = fd_env_strip_cmdline_ulong( &argc, &argv, "--page-cnt", NULL, 96UL ); + ulong numa_idx = fd_env_strip_cmdline_ulong( &argc, &argv, "--numa-idx", NULL, fd_shmem_numa_idx( cpu_idx ) ); + + fd_wksp_t * wksp = fd_wksp_new_anonymous( fd_cstr_to_shmem_page_sz( _page_sz ), page_cnt, fd_shmem_cpu_idx( numa_idx ), "wksp", 0UL ); + FD_TEST( wksp ); + + test_data_path( wksp ); + test_missing_builder_fee_info( wksp ); + + fd_wksp_delete_anonymous( wksp ); + + FD_LOG_NOTICE(( "pass" )); + fd_halt(); + return 0; +} diff --git a/src/disco/plugin/test_bundle_crank.c b/src/disco/bundle/test_bundle_crank.c similarity index 99% rename from src/disco/plugin/test_bundle_crank.c rename to src/disco/bundle/test_bundle_crank.c index 11de15687e..308a40bb55 100644 --- a/src/disco/plugin/test_bundle_crank.c +++ b/src/disco/bundle/test_bundle_crank.c @@ -6,7 +6,7 @@ /* crank2: */ FD_IMPORT_BINARY( payload_2ni, - "src/disco/plugin/fixtures/2niAy2BYpfqTuBM7pZtTkHFnWM2x7bFcFL1oyR1ixqGRcG5uydjZiG5AR8PRYHAaQ3JqA8JYyCRoc3VCwohQVwYP.bin" ); + "src/disco/bundle/fixtures/2niAy2BYpfqTuBM7pZtTkHFnWM2x7bFcFL1oyR1ixqGRcG5uydjZiG5AR8PRYHAaQ3JqA8JYyCRoc3VCwohQVwYP.bin" ); FD_STATIC_ASSERT( sizeof(fd_bundle_crank_2_t)==FD_BUNDLE_CRANK_2_SZ, crank ); FD_STATIC_ASSERT( sizeof(fd_bundle_crank_3_t)==FD_BUNDLE_CRANK_3_SZ, crank ); diff --git a/src/disco/bundle/test_bundle_response.binpb b/src/disco/bundle/test_bundle_response.binpb new file mode 100644 index 0000000000..9c89bfaa9c Binary files /dev/null and b/src/disco/bundle/test_bundle_response.binpb differ diff --git a/src/disco/gui/fd_gui.c b/src/disco/gui/fd_gui.c index 7ed440baec..cf559a1402 100644 --- a/src/disco/gui/fd_gui.c +++ b/src/disco/gui/fd_gui.c @@ -1574,10 +1574,17 @@ static void fd_gui_handle_block_engine_update( fd_gui_t * gui, uchar const * msg ) { fd_plugin_msg_block_engine_update_t const * update = (fd_plugin_msg_block_engine_update_t const *)msg; + fd_topo_t const * topo = gui->topo; + fd_topo_tile_t const * bundle = &topo->tiles[ fd_topo_find_tile( topo, "bundle", 0UL ) ]; + ulong volatile * bundle_metrics = fd_metrics_tile( bundle->metrics ); gui->block_engine.has_block_engine = 1; strncpy( gui->block_engine.name, update->name, sizeof(gui->block_engine.name) ); strncpy( gui->block_engine.url, update->url, sizeof(gui->block_engine.url) ); + + /* Last status reported in metrics, or */ + ulong status = bundle_metrics[ MIDX( GAUGE, BUNDLE, CONNECTION_STATUS ) ]; + ulong heartbeat = bundle_metrics[ MIDX( GAUGE, TILE, HEARTBEAT ) ]; gui->block_engine.status = update->status; fd_gui_printf_block_engine( gui ); diff --git a/src/disco/keyguard/fd_keyguard_authorize.c b/src/disco/keyguard/fd_keyguard_authorize.c index 033d374d09..e0bc7eb993 100644 --- a/src/disco/keyguard/fd_keyguard_authorize.c +++ b/src/disco/keyguard/fd_keyguard_authorize.c @@ -1,6 +1,6 @@ #include "fd_keyguard.h" #include "fd_keyguard_client.h" -#include "../plugin/fd_bundle_crank_constants.h" +#include "../bundle/fd_bundle_crank_constants.h" struct fd_keyguard_sign_req { fd_keyguard_authority_t * authority; diff --git a/src/disco/keyguard/fd_keyload.c b/src/disco/keyguard/fd_keyload.c index 1267f3c016..beee121e28 100644 --- a/src/disco/keyguard/fd_keyload.c +++ b/src/disco/keyguard/fd_keyload.c @@ -83,7 +83,7 @@ uchar * FD_FN_SENSITIVE fd_keyload_load( char const * key_path, int public_key_only ) { if( FD_UNLIKELY( !key_path || !key_path[0] ) ) { - FD_LOG_ERR(( "Invalid key_path" )); + FD_LOG_ERR(( "Missing key_path" )); } /* Load the signing key. Since this is key material, we load it into diff --git a/src/disco/metrics/Makefile b/src/disco/metrics/Makefile new file mode 100644 index 0000000000..d98c933d17 --- /dev/null +++ b/src/disco/metrics/Makefile @@ -0,0 +1,5 @@ +PYTHON?=python3 + +.PHONY: metrics +metrics: + $(PYTHON) ./gen_metrics.py diff --git a/src/disco/metrics/generated/fd_metrics_bundle.c b/src/disco/metrics/generated/fd_metrics_bundle.c index 5f18792a09..3b9927e5db 100644 --- a/src/disco/metrics/generated/fd_metrics_bundle.c +++ b/src/disco/metrics/generated/fd_metrics_bundle.c @@ -5,4 +5,14 @@ const fd_metrics_meta_t FD_METRICS_BUNDLE[FD_METRICS_BUNDLE_TOTAL] = { DECLARE_METRIC( BUNDLE_TRANSACTION_RECEIVED, COUNTER ), DECLARE_METRIC( BUNDLE_PACKET_RECEIVED, COUNTER ), DECLARE_METRIC( BUNDLE_BUNDLE_RECEIVED, COUNTER ), + DECLARE_METRIC_ENUM( BUNDLE_ERRORS, COUNTER, BUNDLE_ERROR, PROTOBUF ), + DECLARE_METRIC_ENUM( BUNDLE_ERRORS, COUNTER, BUNDLE_ERROR, TRANSPORT ), + DECLARE_METRIC_ENUM( BUNDLE_ERRORS, COUNTER, BUNDLE_ERROR, TIMEOUT ), + DECLARE_METRIC_ENUM( BUNDLE_ERRORS, COUNTER, BUNDLE_ERROR, NO_FEE_INFO ), + DECLARE_METRIC_ENUM( BUNDLE_ERRORS, COUNTER, BUNDLE_ERROR, SSL_ALLOC ), + DECLARE_METRIC( BUNDLE_HEAP_SIZE, GAUGE ), + DECLARE_METRIC( BUNDLE_HEAP_FREE_BYTES, GAUGE ), + DECLARE_METRIC( BUNDLE_SHREDSTREAM_HEARTBEATS, COUNTER ), + DECLARE_METRIC( BUNDLE_KEEPALIVES, COUNTER ), + DECLARE_METRIC( BUNDLE_CONNECTED, GAUGE ), }; diff --git a/src/disco/metrics/generated/fd_metrics_bundle.h b/src/disco/metrics/generated/fd_metrics_bundle.h index 3ec64cd47a..0bcc26b097 100644 --- a/src/disco/metrics/generated/fd_metrics_bundle.h +++ b/src/disco/metrics/generated/fd_metrics_bundle.h @@ -21,5 +21,48 @@ #define FD_METRICS_COUNTER_BUNDLE_BUNDLE_RECEIVED_DESC "Total count of bundles received" #define FD_METRICS_COUNTER_BUNDLE_BUNDLE_RECEIVED_CVT (FD_METRICS_CONVERTER_NONE) -#define FD_METRICS_BUNDLE_TOTAL (3UL) +#define FD_METRICS_COUNTER_BUNDLE_ERRORS_OFF (19UL) +#define FD_METRICS_COUNTER_BUNDLE_ERRORS_NAME "bundle_errors" +#define FD_METRICS_COUNTER_BUNDLE_ERRORS_TYPE (FD_METRICS_TYPE_COUNTER) +#define FD_METRICS_COUNTER_BUNDLE_ERRORS_DESC "Number of gRPC errors encountered" +#define FD_METRICS_COUNTER_BUNDLE_ERRORS_CVT (FD_METRICS_CONVERTER_NONE) +#define FD_METRICS_COUNTER_BUNDLE_ERRORS_CNT (5UL) + +#define FD_METRICS_COUNTER_BUNDLE_ERRORS_PROTOBUF_OFF (19UL) +#define FD_METRICS_COUNTER_BUNDLE_ERRORS_TRANSPORT_OFF (20UL) +#define FD_METRICS_COUNTER_BUNDLE_ERRORS_TIMEOUT_OFF (21UL) +#define FD_METRICS_COUNTER_BUNDLE_ERRORS_NO_FEE_INFO_OFF (22UL) +#define FD_METRICS_COUNTER_BUNDLE_ERRORS_SSL_ALLOC_OFF (23UL) + +#define FD_METRICS_GAUGE_BUNDLE_HEAP_SIZE_OFF (24UL) +#define FD_METRICS_GAUGE_BUNDLE_HEAP_SIZE_NAME "bundle_heap_size" +#define FD_METRICS_GAUGE_BUNDLE_HEAP_SIZE_TYPE (FD_METRICS_TYPE_GAUGE) +#define FD_METRICS_GAUGE_BUNDLE_HEAP_SIZE_DESC "Workspace heap size" +#define FD_METRICS_GAUGE_BUNDLE_HEAP_SIZE_CVT (FD_METRICS_CONVERTER_NONE) + +#define FD_METRICS_GAUGE_BUNDLE_HEAP_FREE_BYTES_OFF (25UL) +#define FD_METRICS_GAUGE_BUNDLE_HEAP_FREE_BYTES_NAME "bundle_heap_free_bytes" +#define FD_METRICS_GAUGE_BUNDLE_HEAP_FREE_BYTES_TYPE (FD_METRICS_TYPE_GAUGE) +#define FD_METRICS_GAUGE_BUNDLE_HEAP_FREE_BYTES_DESC "Approx free space in workspace" +#define FD_METRICS_GAUGE_BUNDLE_HEAP_FREE_BYTES_CVT (FD_METRICS_CONVERTER_NONE) + +#define FD_METRICS_COUNTER_BUNDLE_SHREDSTREAM_HEARTBEATS_OFF (26UL) +#define FD_METRICS_COUNTER_BUNDLE_SHREDSTREAM_HEARTBEATS_NAME "bundle_shredstream_heartbeats" +#define FD_METRICS_COUNTER_BUNDLE_SHREDSTREAM_HEARTBEATS_TYPE (FD_METRICS_TYPE_COUNTER) +#define FD_METRICS_COUNTER_BUNDLE_SHREDSTREAM_HEARTBEATS_DESC "Number of ShredStream heartbeats successfully sent" +#define FD_METRICS_COUNTER_BUNDLE_SHREDSTREAM_HEARTBEATS_CVT (FD_METRICS_CONVERTER_NONE) + +#define FD_METRICS_COUNTER_BUNDLE_KEEPALIVES_OFF (27UL) +#define FD_METRICS_COUNTER_BUNDLE_KEEPALIVES_NAME "bundle_keepalives" +#define FD_METRICS_COUNTER_BUNDLE_KEEPALIVES_TYPE (FD_METRICS_TYPE_COUNTER) +#define FD_METRICS_COUNTER_BUNDLE_KEEPALIVES_DESC "Number of HTTP/2 PINGs acknowledged by server" +#define FD_METRICS_COUNTER_BUNDLE_KEEPALIVES_CVT (FD_METRICS_CONVERTER_NONE) + +#define FD_METRICS_GAUGE_BUNDLE_CONNECTED_OFF (28UL) +#define FD_METRICS_GAUGE_BUNDLE_CONNECTED_NAME "bundle_connected" +#define FD_METRICS_GAUGE_BUNDLE_CONNECTED_TYPE (FD_METRICS_TYPE_GAUGE) +#define FD_METRICS_GAUGE_BUNDLE_CONNECTED_DESC "1 if connected to the bundle server, 0 if not" +#define FD_METRICS_GAUGE_BUNDLE_CONNECTED_CVT (FD_METRICS_CONVERTER_NONE) + +#define FD_METRICS_BUNDLE_TOTAL (13UL) extern const fd_metrics_meta_t FD_METRICS_BUNDLE[FD_METRICS_BUNDLE_TOTAL]; diff --git a/src/disco/metrics/generated/fd_metrics_enums.h b/src/disco/metrics/generated/fd_metrics_enums.h index 9742be60c0..703650f987 100644 --- a/src/disco/metrics/generated/fd_metrics_enums.h +++ b/src/disco/metrics/generated/fd_metrics_enums.h @@ -127,6 +127,19 @@ #define FD_METRICS_ENUM_QUIC_ENC_LEVEL_V_APP_IDX 3 #define FD_METRICS_ENUM_QUIC_ENC_LEVEL_V_APP_NAME "app" +#define FD_METRICS_ENUM_BUNDLE_ERROR_NAME "bundle_error" +#define FD_METRICS_ENUM_BUNDLE_ERROR_CNT (5UL) +#define FD_METRICS_ENUM_BUNDLE_ERROR_V_PROTOBUF_IDX 0 +#define FD_METRICS_ENUM_BUNDLE_ERROR_V_PROTOBUF_NAME "protobuf" +#define FD_METRICS_ENUM_BUNDLE_ERROR_V_TRANSPORT_IDX 1 +#define FD_METRICS_ENUM_BUNDLE_ERROR_V_TRANSPORT_NAME "transport" +#define FD_METRICS_ENUM_BUNDLE_ERROR_V_TIMEOUT_IDX 2 +#define FD_METRICS_ENUM_BUNDLE_ERROR_V_TIMEOUT_NAME "timeout" +#define FD_METRICS_ENUM_BUNDLE_ERROR_V_NO_FEE_INFO_IDX 3 +#define FD_METRICS_ENUM_BUNDLE_ERROR_V_NO_FEE_INFO_NAME "no_fee_info" +#define FD_METRICS_ENUM_BUNDLE_ERROR_V_SSL_ALLOC_IDX 4 +#define FD_METRICS_ENUM_BUNDLE_ERROR_V_SSL_ALLOC_NAME "ssl_alloc" + #define FD_METRICS_ENUM_LUT_RESOLVE_RESULT_NAME "lut_resolve_result" #define FD_METRICS_ENUM_LUT_RESOLVE_RESULT_CNT (6UL) #define FD_METRICS_ENUM_LUT_RESOLVE_RESULT_V_INVALID_LOOKUP_INDEX_IDX 0 diff --git a/src/disco/metrics/metrics.xml b/src/disco/metrics/metrics.xml index b7351b9662..5199953c6d 100644 --- a/src/disco/metrics/metrics.xml +++ b/src/disco/metrics/metrics.xml @@ -218,10 +218,24 @@ metric introduced. + + + + + + + + + + + + + + diff --git a/src/disco/plugin/Local.mk b/src/disco/plugin/Local.mk index a0b57225cc..35f92d9b1e 100644 --- a/src/disco/plugin/Local.mk +++ b/src/disco/plugin/Local.mk @@ -1,9 +1,5 @@ -$(call add-hdrs,fd_bundle_crank.h) ifdef FD_HAS_INT128 ifdef FD_HAS_SSE -$(call add-objs,fd_bundle_crank fd_plugin_tile fd_bundle_tile,fd_disco,fd_flamenco) - -$(call make-unit-test,test_bundle_crank,test_bundle_crank,fd_disco fd_flamenco fd_ballet fd_util) -$(call run-unit-test,test_bundle_crank,) +$(call add-objs,fd_plugin_tile,fd_disco,fd_flamenco) endif endif diff --git a/src/disco/plugin/fd_bundle_tile.c b/src/disco/plugin/fd_bundle_tile.c deleted file mode 100644 index 1accf9e99c..0000000000 --- a/src/disco/plugin/fd_bundle_tile.c +++ /dev/null @@ -1,446 +0,0 @@ -#define _DEFAULT_SOURCE -#include "..//tiles.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include "generated/fd_bundle_tile_seccomp.h" - -#include "../keyguard/fd_keyload.h" -#include "../keyguard/fd_keyguard.h" -#include "../keyguard/fd_keyguard_client.h" -#include "../keyguard/fd_keyswitch.h" -#include "../plugin/fd_plugin.h" -#include "../metrics/fd_metrics.h" -#include "../../util/net/fd_ip4.h" - -#include "../verify/fd_verify_tile.h" - -#include -#include -#include - -typedef struct { - ulong idx; - fd_wksp_t * mem; - ulong chunk0; - ulong wmark; - ulong chunk; -} fd_bundle_out_ctx_t; - -/* fd_bundle_ctx_t is the context object provided to callbacks from the - mux tile, and contains all state needed to progress the tile. */ - -typedef struct { - fd_keyguard_client_t keyguard_client[1]; - - int identity_switched; - fd_keyswitch_t * keyswitch; - uchar identity_public_key[ 32UL ]; - - ulong bundle_id; - - int plugin_initialized; - - char url[ 256 ]; - char domain_name[ 256 ]; - - void * plugin; - - fd_bundle_out_ctx_t verify_out; - fd_bundle_out_ctx_t plugin_out; - - struct { - ulong txn_received; - ulong bundle_received; - ulong packet_received; - } metrics; -} fd_bundle_ctx_t; - -FD_FN_CONST static inline ulong -scratch_align( void ) { - return alignof( fd_bundle_ctx_t ); -} - -FD_FN_PURE static inline ulong -scratch_footprint( fd_topo_tile_t const * tile ) { - (void)tile; - ulong l = FD_LAYOUT_INIT; - l = FD_LAYOUT_APPEND( l, alignof( fd_bundle_ctx_t ), sizeof( fd_bundle_ctx_t ) ); - return FD_LAYOUT_FINI( l, scratch_align() ); -} - -static inline void -during_housekeeping( fd_bundle_ctx_t * ctx ) { - if( FD_UNLIKELY( fd_keyswitch_state_query( ctx->keyswitch )==FD_KEYSWITCH_STATE_SWITCH_PENDING ) ) { - ctx->identity_switched = 1; - fd_memcpy( ctx->identity_public_key, ctx->keyswitch->bytes, 32UL ); - fd_keyswitch_state( ctx->keyswitch, FD_KEYSWITCH_STATE_COMPLETED ); - } -} - -static inline void -metrics_write( fd_bundle_ctx_t * ctx ) { - FD_MCNT_SET( BUNDLE, TRANSACTION_RECEIVED, ctx->metrics.txn_received ); - FD_MCNT_SET( BUNDLE, BUNDLE_RECEIVED, ctx->metrics.bundle_received ); - FD_MCNT_SET( BUNDLE, PACKET_RECEIVED, ctx->metrics.packet_received ); -} - -extern void -plugin_bundle_poll( void * plugin, - int reload_identity, - uchar * identity_pubkey, - int * out_type, - uchar * out_block_builder_pubkey, - ulong * out_block_builder_commission, - ulong * out_bundle_len, - uchar * out_data ); - -extern void * -plugin_bundle_init( char const * url, - char const * domain_name, - uchar * identity_pubkey ); - -static FD_TL fd_bundle_ctx_t * tl_bundle_ctx; - -void -plugin_bundle_sign_challenge( char const * challenge, - uchar * out_signature ) { - fd_bundle_ctx_t * ctx = tl_bundle_ctx; - fd_keyguard_client_sign( ctx->keyguard_client, out_signature, (const uchar *)challenge, 9UL, FD_KEYGUARD_SIGN_TYPE_PUBKEY_CONCAT_ED25519 ); -} - -struct fd_bundle_msg { - ulong txn_cnt; - fd_txn_p_t txns[ 5 ]; -}; - -typedef struct fd_bundle_msg fd_bundle_msg_t; - -static inline void -after_credit( fd_bundle_ctx_t * ctx, - fd_stem_context_t * stem, - int * opt_poll_in, - int * charge_busy ) { - (void)opt_poll_in; - - if( FD_UNLIKELY( !ctx->plugin_initialized ) ) { - ctx->plugin_initialized = 1; - - if( FD_LIKELY( ctx->plugin_out.mem ) ) { - fd_plugin_msg_block_engine_update_t * update = (fd_plugin_msg_block_engine_update_t *)fd_chunk_to_laddr( ctx->plugin_out.mem, ctx->plugin_out.chunk ); - strncpy( update->url, ctx->url, sizeof(update->url) ); - strncpy( update->name, "jito", sizeof(update->name) ); - update->status = FD_PLUGIN_MSG_BLOCK_ENGINE_UPDATE_STATUS_DISCONNECTED; - - ulong tspub = (ulong)fd_frag_meta_ts_comp( fd_tickcount() ); - fd_stem_publish( stem, ctx->plugin_out.idx, FD_PLUGIN_MSG_BLOCK_ENGINE_UPDATE, ctx->plugin_out.chunk, sizeof(fd_plugin_msg_block_engine_update_t), 0UL, 0UL, tspub ); - ctx->plugin_out.chunk = fd_dcache_compact_next( ctx->plugin_out.chunk, sizeof(fd_plugin_msg_block_engine_update_t), ctx->plugin_out.chunk0, ctx->plugin_out.wmark ); - - return; - } - } - - fd_bundle_msg_t * msg = fd_chunk_to_laddr( ctx->verify_out.mem, ctx->verify_out.chunk ); - uchar data[ 5UL*(8UL+FD_TXN_MTU) ]; - - int type; - ulong block_builder_commission; - uchar block_builder_pubkey[ 32UL ]; - - plugin_bundle_poll( ctx->plugin, ctx->identity_switched, ctx->identity_public_key, &type, block_builder_pubkey, &block_builder_commission, &msg->txn_cnt, data ); - ctx->identity_switched = 0; - if( FD_LIKELY( !type ) ) return; - - *charge_busy = 1; - - if( FD_UNLIKELY( type<0 ) ) { - if( FD_LIKELY( ctx->plugin_out.mem ) ) { - fd_plugin_msg_block_engine_update_t * update = (fd_plugin_msg_block_engine_update_t *)fd_chunk_to_laddr( ctx->plugin_out.mem, ctx->plugin_out.chunk ); - strncpy( update->url, ctx->url, sizeof(update->url) ); - strncpy( update->name, "jito", sizeof(update->name) ); - - switch( type ) { - case -1: { - update->status = FD_PLUGIN_MSG_BLOCK_ENGINE_UPDATE_STATUS_DISCONNECTED; - break; - } - case -2: { - update->status = FD_PLUGIN_MSG_BLOCK_ENGINE_UPDATE_STATUS_CONNECTING; - break; - } - case -3: { - update->status = FD_PLUGIN_MSG_BLOCK_ENGINE_UPDATE_STATUS_CONNECTED; - break; - } - default: - FD_LOG_ERR(( "invalid plugin_bundle_poll return value %d", type )); - } - - ulong tspub = (ulong)fd_frag_meta_ts_comp( fd_tickcount() ); - fd_stem_publish( stem, ctx->plugin_out.idx, FD_PLUGIN_MSG_BLOCK_ENGINE_UPDATE, ctx->plugin_out.chunk, sizeof(fd_plugin_msg_block_engine_update_t), 0UL, 0UL, tspub ); - ctx->plugin_out.chunk = fd_dcache_compact_next( ctx->plugin_out.chunk, sizeof(fd_plugin_msg_block_engine_update_t), ctx->plugin_out.chunk0, ctx->plugin_out.wmark ); - } - return; - } - - if( FD_UNLIKELY( !msg->txn_cnt ) ) return; - - if( FD_UNLIKELY( msg->txn_cnt>5UL ) ) { - FD_LOG_WARNING(( "bundle plugin produced invalid bundle of length %lu", msg->txn_cnt )); - return; - } - if( FD_UNLIKELY( block_builder_commission>100UL ) ) { - FD_LOG_WARNING(( "bundle plugin produced invalid commission %lu", block_builder_commission )); - return; - } - - ulong offset = 0UL; - for( ulong i=0UL; itxn_cnt; i++ ) { - ulong payload_sz = fd_ulong_load_8( data+offset ); - if( FD_UNLIKELY( payload_sz>FD_TXN_MTU ) ) { - FD_LOG_WARNING(( "bundle plugin produced invalid payload size %lu", payload_sz )); - return; - } - offset += 8UL+payload_sz; - } - - ctx->metrics.txn_received += msg->txn_cnt; - if( FD_LIKELY( type==2 ) ) ctx->metrics.packet_received++; - else ctx->metrics.bundle_received++; - - /* Increment, skip 0 on overflow */ - ctx->bundle_id = fd_ulong_max( ctx->bundle_id+1UL, 1UL ); - - offset = 0UL; - for( ulong i=0UL; itxn_cnt; i++ ) { - ulong payload_sz = fd_ulong_load_8( data+offset ); - offset += 8UL; - uchar const * payload = data+offset; - offset += payload_sz; - - fd_txn_m_t * txnm = (fd_txn_m_t *)fd_chunk_to_laddr( ctx->verify_out.mem, ctx->verify_out.chunk ); - - if( FD_LIKELY( type==2 ) ) txnm->block_engine.bundle_id = 0UL; - else txnm->block_engine.bundle_id = ctx->bundle_id; - - if( FD_LIKELY( i==0UL ) ) { - txnm->block_engine.bundle_txn_cnt = msg->txn_cnt; - txnm->block_engine.commission = (uchar)block_builder_commission; - fd_memcpy( txnm->block_engine.commission_pubkey, block_builder_pubkey, 32UL ); - } else { - txnm->block_engine.bundle_txn_cnt = 0UL; - } - txnm->payload_sz = (ushort)payload_sz; - fd_memcpy( fd_txn_m_payload( txnm ), payload, payload_sz ); - - ulong footprint = fd_txn_m_realized_footprint( txnm, 0, 0 ); - - ulong tspub = (ulong)fd_frag_meta_ts_comp( fd_tickcount() ); - fd_stem_publish( stem, ctx->verify_out.idx, type!=2, ctx->verify_out.chunk, footprint, 0UL, 0UL, tspub ); - ctx->verify_out.chunk = fd_dcache_compact_next( ctx->verify_out.chunk, footprint, ctx->verify_out.chunk0, ctx->verify_out.wmark ); - } -} - -static void -replace_domain_with_ip( char const * url, - char * domain, - char * result ) { - const char * protocol_end = strstr( url, "://" ); - if( FD_UNLIKELY( !protocol_end ) ) FD_LOG_ERR(( "invalid [tiles.bundle.url] `%s`. Must start with `http[s]://`", url )); - - char const * domain_start = protocol_end+3UL; - /* Check for a port number */ - char const * domain_end = strchr( domain_start, ':' ); - if( FD_LIKELY( !domain_end ) ) domain_end = url + strlen( url ); - - ulong domain_len = (ulong)(domain_end-domain_start); - if( FD_UNLIKELY( domain_len>=256UL ) ) FD_LOG_ERR(( "[tiles.bundle.url] `%s` is too long (%lu>255)", url, domain_len )); - strncpy( domain, domain_start, domain_len ); - domain[ domain_len ] = '\0'; - - struct addrinfo hints, *res; - memset( &hints, 0, sizeof(hints) ); - hints.ai_family = AF_INET; - - int err = getaddrinfo( domain, NULL, &hints, &res ); - if( FD_UNLIKELY( err ) ) FD_LOG_ERR(( "getaddrinfo `%s` failed (%d-%s)", domain, err, gai_strerror( err ) )); - - uint ip_addr = ((struct sockaddr_in *)res->ai_addr)->sin_addr.s_addr; - FD_TEST( fd_cstr_printf_check( result, 256, NULL, "%.*s" FD_IP4_ADDR_FMT "%s", (int)(protocol_end-url+3L), url, FD_IP4_ADDR_FMT_ARGS( ip_addr ), domain_end ) ); - freeaddrinfo(res); -} - -static void -privileged_init( fd_topo_t * topo, - fd_topo_tile_t * tile ) { - void * scratch = fd_topo_obj_laddr( topo, tile->tile_obj_id ); - - FD_SCRATCH_ALLOC_INIT( l, scratch ); - fd_bundle_ctx_t * ctx = FD_SCRATCH_ALLOC_APPEND( l, alignof( fd_bundle_ctx_t ), sizeof( fd_bundle_ctx_t ) ); - - uchar const * public_key = fd_keyload_load( tile->bundle.identity_key_path, 1 /* public key only */ ); - fd_memcpy( ctx->identity_public_key, public_key, 32UL ); - - if( FD_UNLIKELY( strlen( tile->bundle.url)<8UL || (strncmp( "https://", tile->bundle.url, 8UL ) && strncmp( "http://", tile->bundle.url, 7UL )) )) FD_LOG_ERR(( "invalid [tiles.bundle.url] `%s`. Must start with `http[s]://`", tile->bundle.url )); - - /* DNS resolution loads files, `resolv.conf` and all kind of rubbish - like that, don't want to allow in the sandbox. */ - replace_domain_with_ip( tile->bundle.url, ctx->domain_name, ctx->url ); - if( FD_UNLIKELY( strcmp( tile->bundle.tls_domain_name, "" ) ) ) { - strncpy( ctx->domain_name, tile->bundle.tls_domain_name, sizeof(ctx->domain_name) ); - } -} - -static inline fd_bundle_out_ctx_t -out1( fd_topo_t const * topo, - fd_topo_tile_t const * tile, - char const * name ) { - ulong idx = ULONG_MAX; - - for( ulong i=0UL; iout_cnt; i++ ) { - fd_topo_link_t const * link = &topo->links[ tile->out_link_id[ i ] ]; - if( !strcmp( link->name, name ) ) { - if( FD_UNLIKELY( idx!=ULONG_MAX ) ) FD_LOG_ERR(( "tile %s:%lu had multiple output links named %s but expected one", tile->name, tile->kind_id, name )); - idx = i; - } - } - - if( FD_UNLIKELY( idx==ULONG_MAX ) ) FD_LOG_ERR(( "tile %s:%lu had no output link named %s", tile->name, tile->kind_id, name )); - - void * mem = topo->workspaces[ topo->objs[ topo->links[ tile->out_link_id[ idx ] ].dcache_obj_id ].wksp_id ].wksp; - ulong chunk0 = fd_dcache_compact_chunk0( mem, topo->links[ tile->out_link_id[ idx ] ].dcache ); - ulong wmark = fd_dcache_compact_wmark ( mem, topo->links[ tile->out_link_id[ idx ] ].dcache, topo->links[ tile->out_link_id[ idx ] ].mtu ); - - return (fd_bundle_out_ctx_t){ .idx = idx, .mem = mem, .chunk0 = chunk0, .wmark = wmark, .chunk = chunk0 }; -} - -static inline fd_bundle_out_ctx_t -out1opt( fd_topo_t const * topo, - fd_topo_tile_t const * tile, - char const * name ) { - ulong idx = ULONG_MAX; - - for( ulong i=0UL; iout_cnt; i++ ) { - fd_topo_link_t const * link = &topo->links[ tile->out_link_id[ i ] ]; - if( !strcmp( link->name, name ) ) { - if( FD_UNLIKELY( idx!=ULONG_MAX ) ) FD_LOG_ERR(( "tile %s:%lu had multiple output links named %s but expected one", tile->name, tile->kind_id, name )); - idx = i; - } - } - - if( FD_UNLIKELY( idx==ULONG_MAX ) ) { - return (fd_bundle_out_ctx_t){ .idx = ULONG_MAX, .mem = NULL, .chunk0 = 0UL, .wmark = 0UL, .chunk = 0UL }; - } - - void * mem = topo->workspaces[ topo->objs[ topo->links[ tile->out_link_id[ idx ] ].dcache_obj_id ].wksp_id ].wksp; - ulong chunk0 = fd_dcache_compact_chunk0( mem, topo->links[ tile->out_link_id[ idx ] ].dcache ); - ulong wmark = fd_dcache_compact_wmark ( mem, topo->links[ tile->out_link_id[ idx ] ].dcache, topo->links[ tile->out_link_id[ idx ] ].mtu ); - - return (fd_bundle_out_ctx_t){ .idx = idx, .mem = mem, .chunk0 = chunk0, .wmark = wmark, .chunk = chunk0 }; -} - -static void -unprivileged_init( fd_topo_t * topo, - fd_topo_tile_t * tile ) { - void * scratch = fd_topo_obj_laddr( topo, tile->tile_obj_id ); - - FD_SCRATCH_ALLOC_INIT( l, scratch ); - fd_bundle_ctx_t * ctx = FD_SCRATCH_ALLOC_APPEND( l, alignof( fd_bundle_ctx_t ), sizeof( fd_bundle_ctx_t ) ); - - ulong sign_in_idx = fd_topo_find_tile_in_link( topo, tile, "sign_bundle", tile->kind_id ); - FD_TEST( sign_in_idx!=ULONG_MAX ); - fd_topo_link_t * sign_in = &topo->links[ tile->in_link_id[ sign_in_idx ] ]; - fd_topo_link_t * sign_out = &topo->links[ tile->out_link_id[ 1UL ] ]; - FD_TEST( !strcmp( sign_out->name, "bundle_sign" ) ); - - if( FD_UNLIKELY( !fd_keyguard_client_join( fd_keyguard_client_new( ctx->keyguard_client, - sign_out->mcache, - sign_out->dcache, - sign_in->mcache, - sign_in->dcache ) ) ) ) { - FD_LOG_ERR(( "fd_keyguard_client_join failed" )); - } - - tl_bundle_ctx = ctx; - - ctx->identity_switched = 0; - ctx->keyswitch = fd_keyswitch_join( fd_topo_obj_laddr( topo, tile->keyswitch_obj_id ) ); - FD_TEST( ctx->keyswitch ); - - ctx->plugin = plugin_bundle_init( ctx->url, ctx->domain_name, ctx->identity_public_key ); - FD_TEST( ctx->plugin ); - - ctx->plugin_initialized = 0; - ctx->bundle_id = 0UL; - - ctx->verify_out = out1( topo, tile, "bundle_verif" ); - ctx->plugin_out = out1opt( topo, tile, "bundle_plugi" ); - - memset( &ctx->metrics, 0, sizeof( ctx->metrics ) ); - - ulong scratch_top = FD_SCRATCH_ALLOC_FINI( l, scratch_align() ); - if( FD_UNLIKELY( scratch_top > (ulong)scratch + scratch_footprint( tile ) ) ) - FD_LOG_ERR(( "scratch overflow %lu %lu %lu", scratch_top - (ulong)scratch - scratch_footprint( tile ), scratch_top, (ulong)scratch + scratch_footprint( tile ) )); -} - -static ulong -populate_allowed_seccomp( fd_topo_t const * topo, - fd_topo_tile_t const * tile, - ulong out_cnt, - struct sock_filter * out ) { - (void)topo; - (void)tile; - - populate_sock_filter_policy_fd_bundle_tile( out_cnt, out, (uint)fd_log_private_logfile_fd() ); - return sock_filter_policy_fd_bundle_tile_instr_cnt; -} - -static ulong -populate_allowed_fds( fd_topo_t const * topo, - fd_topo_tile_t const * tile, - ulong out_fds_cnt, - int * out_fds ) { - (void)topo; - (void)tile; - - if( FD_UNLIKELY( out_fds_cnt<2UL ) ) FD_LOG_ERR(( "out_fds_cnt %lu", out_fds_cnt )); - - ulong out_cnt = 0UL; - out_fds[ out_cnt++ ] = 2; /* stderr */ - if( FD_LIKELY( -1!=fd_log_private_logfile_fd() ) ) - out_fds[ out_cnt++ ] = fd_log_private_logfile_fd(); /* logfile */ - return out_cnt; -} - -#define STEM_BURST (5UL) - -#define STEM_CALLBACK_CONTEXT_TYPE fd_bundle_ctx_t -#define STEM_CALLBACK_CONTEXT_ALIGN alignof(fd_bundle_ctx_t) - -#define STEM_CALLBACK_DURING_HOUSEKEEPING during_housekeeping -#define STEM_CALLBACK_METRICS_WRITE metrics_write -#define STEM_CALLBACK_AFTER_CREDIT after_credit - -#include "../../disco/stem/fd_stem.c" - -fd_topo_run_tile_t fd_tile_bundle = { - .name = "bundle", - .rlimit_file_cnt = 4096UL, /* Rust side opens a few files for DNS lookups and things */ - .rlimit_address_space = RLIM_INFINITY, - .rlimit_data = RLIM_INFINITY, - .keep_host_networking = 1, /* We need to use the NIC to connect(2) to the block producer endpoint */ - .allow_connect = 1, /* We need to use connect(2) to connect to the block producer endpoint */ - .populate_allowed_seccomp = populate_allowed_seccomp, - .populate_allowed_fds = populate_allowed_fds, - .scratch_align = scratch_align, - .scratch_footprint = scratch_footprint, - .privileged_init = privileged_init, - .unprivileged_init = unprivileged_init, - .run = stem_run, -}; diff --git a/src/disco/plugin/fd_bundle_tile.seccomppolicy b/src/disco/plugin/fd_bundle_tile.seccomppolicy deleted file mode 100644 index 164579b6ae..0000000000 --- a/src/disco/plugin/fd_bundle_tile.seccomppolicy +++ /dev/null @@ -1,91 +0,0 @@ -# logfile_fd: It can be disabled by configuration, but typically tiles -# will open a log file on boot and write all messages there. -unsigned int logfile_fd - -# logging: all log messages are written to a file and/or pipe -# -# 'WARNING' and above are written to the STDERR pipe, while all -# messages are always written to the log file. -# -# arg 0 is the file descriptor to write to. The boot process ensures -# that descriptor 2 is always STDERR. -# -# This is commented out because the bundle code uses write in an -# unrestricted way. -# -# write: (or (eq (arg 0) 2) -# (eq (arg 0) logfile_fd)) - -# logging: 'WARNING' and above fsync the logfile to disk immediately -# -# arg 0 is the file descriptor to fsync. -fsync: (eq (arg 0) logfile_fd) - -# bundle: Rustls uses getrandom to source randomness for the HTTPS -# connection to the block engine. -getrandom - -# bundle: Tokio uses epoll to poll I/O futures representing the TCP -# connection to the block producer. epoll_create1 and eventfd2 -# are initialization time only and could be removed if the Tokio -# I/O was inited during privileged boot. -epoll_create1: (eq (arg 0) "EPOLL_CLOEXEC") -eventfd2: (eq (arg 1) "EFD_CLOEXEC|EFD_NONBLOCK") -epoll_ctl -epoll_wait - -# bundle: TCP connection to the block engine needs to be established. -socket: (and (eq (arg 0) "AF_INET") - (eq (arg 1) "SOCK_STREAM|SOCK_CLOEXEC") - (eq (arg 2) "IPPROTO_TCP")) -connect - -# bundle: Set the TCP socket as non-blocking, and Tokio also uses -# F_DUPFD_CLOEXEC internally for reasons that are unclear. -fcntl: (or (eq (arg 1) "F_GETFL") - (and (eq (arg 1) "F_SETFL") - (eq (arg 2) "O_RDWR|O_NONBLOCK")) - (eq (arg 1) "F_DUPFD_CLOEXEC")) - -# bundle: TCP connection to the block engine needs to configure -# keepalives, and tune for performance. -setsockopt: (or (and (eq (arg 1) "SOL_SOCKET") - (eq (arg 2) "SO_KEEPALIVE")) - (and (eq (arg 1) "SOL_TCP") - (eq (arg 2) "TCP_KEEPIDLE")) - (and (eq (arg 1) "SOL_TCP") - (eq (arg 2) "TCP_NODELAY"))) -getsockopt: (or (and (eq (arg 1) "SOL_TCP") - (eq (arg 2) "TCP_NODELAY")) - (and (eq (arg 1) "SOL_SOCKET") - (eq (arg 2) "SO_ERROR"))) - -# bundle: We need to be able to read and write to the TCP socket to -# communicate with the block producer server. -recvfrom -sendto -write -writev - -# bundle: TCP connection is closed if there is an error, and a new one -# is reopened -shutdown: (eq (arg 1) "SHUT_WR") -close - -# bundle: Global memory allocator used by Rust and Tokio might call mmap -# if it needs more memory. Don't allow mapping executable pages. -mmap: (or (eq (arg 2) "PROT_READ|PROT_WRITE") - (eq (arg 2) "PROT_NONE")) - -# bundle: Global memory allocator used by Rust and Tokio might call brk -# if it needs more memory. -brk - -# bundle: Not sure exactly why these are needed, but the Rust bundle -# receiver needs them. -rt_sigaction -rt_sigprocmask -getpid -gettid -tgkill -madvise diff --git a/src/disco/plugin/generated/fd_bundle_tile_seccomp.h b/src/disco/plugin/generated/fd_bundle_tile_seccomp.h deleted file mode 100644 index 391cb4434b..0000000000 --- a/src/disco/plugin/generated/fd_bundle_tile_seccomp.h +++ /dev/null @@ -1,188 +0,0 @@ -/* THIS FILE WAS GENERATED BY generate_filters.py. DO NOT EDIT BY HAND! */ -#ifndef HEADER_fd_src_disco_plugin_generated_fd_bundle_tile_seccomp_h -#define HEADER_fd_src_disco_plugin_generated_fd_bundle_tile_seccomp_h - -#include "../../../../src/util/fd_util_base.h" -#include -#include -#include -#include -#include -#include -#include -#include - -#if defined(__i386__) -# define ARCH_NR AUDIT_ARCH_I386 -#elif defined(__x86_64__) -# define ARCH_NR AUDIT_ARCH_X86_64 -#elif defined(__aarch64__) -# define ARCH_NR AUDIT_ARCH_AARCH64 -#else -# error "Target architecture is unsupported by seccomp." -#endif -static const unsigned int sock_filter_policy_fd_bundle_tile_instr_cnt = 77; - -static void populate_sock_filter_policy_fd_bundle_tile( ulong out_cnt, struct sock_filter * out, unsigned int logfile_fd) { - FD_TEST( out_cnt >= 77 ); - struct sock_filter filter[77] = { - /* Check: Jump to RET_KILL_PROCESS if the script's arch != the runtime arch */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, ( offsetof( struct seccomp_data, arch ) ) ), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, ARCH_NR, 0, /* RET_KILL_PROCESS */ 73 ), - /* loading syscall number in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, ( offsetof( struct seccomp_data, nr ) ) ), - /* allow fsync based on expression */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_fsync, /* check_fsync */ 25, 0 ), - /* simply allow getrandom */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_getrandom, /* RET_ALLOW */ 71, 0 ), - /* allow epoll_create1 based on expression */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_epoll_create1, /* check_epoll_create1 */ 25, 0 ), - /* allow eventfd2 based on expression */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_eventfd2, /* check_eventfd2 */ 26, 0 ), - /* simply allow epoll_ctl */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_epoll_ctl, /* RET_ALLOW */ 68, 0 ), - /* simply allow epoll_wait */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_epoll_wait, /* RET_ALLOW */ 67, 0 ), - /* allow socket based on expression */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_socket, /* check_socket */ 25, 0 ), - /* simply allow connect */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_connect, /* RET_ALLOW */ 65, 0 ), - /* allow fcntl based on expression */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_fcntl, /* check_fcntl */ 29, 0 ), - /* allow setsockopt based on expression */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_setsockopt, /* check_setsockopt */ 36, 0 ), - /* allow getsockopt based on expression */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_getsockopt, /* check_getsockopt */ 47, 0 ), - /* simply allow recvfrom */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_recvfrom, /* RET_ALLOW */ 61, 0 ), - /* simply allow sendto */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_sendto, /* RET_ALLOW */ 60, 0 ), - /* simply allow write */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_write, /* RET_ALLOW */ 59, 0 ), - /* simply allow writev */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_writev, /* RET_ALLOW */ 58, 0 ), - /* allow shutdown based on expression */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_shutdown, /* check_shutdown */ 50, 0 ), - /* simply allow close */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_close, /* RET_ALLOW */ 56, 0 ), - /* allow mmap based on expression */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_mmap, /* check_mmap */ 50, 0 ), - /* simply allow brk */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_brk, /* RET_ALLOW */ 54, 0 ), - /* simply allow rt_sigaction */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_rt_sigaction, /* RET_ALLOW */ 53, 0 ), - /* simply allow rt_sigprocmask */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_rt_sigprocmask, /* RET_ALLOW */ 52, 0 ), - /* simply allow getpid */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_getpid, /* RET_ALLOW */ 51, 0 ), - /* simply allow gettid */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_gettid, /* RET_ALLOW */ 50, 0 ), - /* simply allow tgkill */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_tgkill, /* RET_ALLOW */ 49, 0 ), - /* simply allow madvise */ - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SYS_madvise, /* RET_ALLOW */ 48, 0 ), - /* none of the syscalls matched */ - { BPF_JMP | BPF_JA, 0, 0, /* RET_KILL_PROCESS */ 46 }, -// check_fsync: - /* load syscall argument 0 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, logfile_fd, /* RET_ALLOW */ 45, /* RET_KILL_PROCESS */ 44 ), -// check_epoll_create1: - /* load syscall argument 0 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, EPOLL_CLOEXEC, /* RET_ALLOW */ 43, /* RET_KILL_PROCESS */ 42 ), -// check_eventfd2: - /* load syscall argument 1 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, EFD_CLOEXEC|EFD_NONBLOCK, /* RET_ALLOW */ 41, /* RET_KILL_PROCESS */ 40 ), -// check_socket: - /* load syscall argument 0 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, AF_INET, /* lbl_1 */ 0, /* RET_KILL_PROCESS */ 38 ), -// lbl_1: - /* load syscall argument 1 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SOCK_STREAM|SOCK_CLOEXEC, /* lbl_2 */ 0, /* RET_KILL_PROCESS */ 36 ), -// lbl_2: - /* load syscall argument 2 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_TCP, /* RET_ALLOW */ 35, /* RET_KILL_PROCESS */ 34 ), -// check_fcntl: - /* load syscall argument 1 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, F_GETFL, /* RET_ALLOW */ 33, /* lbl_3 */ 0 ), -// lbl_3: - /* load syscall argument 1 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, F_SETFL, /* lbl_5 */ 0, /* lbl_4 */ 2 ), -// lbl_5: - /* load syscall argument 2 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, O_RDWR|O_NONBLOCK, /* RET_ALLOW */ 29, /* lbl_4 */ 0 ), -// lbl_4: - /* load syscall argument 1 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, F_DUPFD_CLOEXEC, /* RET_ALLOW */ 27, /* RET_KILL_PROCESS */ 26 ), -// check_setsockopt: - /* load syscall argument 1 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SOL_SOCKET, /* lbl_7 */ 0, /* lbl_6 */ 2 ), -// lbl_7: - /* load syscall argument 2 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SO_KEEPALIVE, /* RET_ALLOW */ 23, /* lbl_6 */ 0 ), -// lbl_6: - /* load syscall argument 1 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SOL_TCP, /* lbl_9 */ 0, /* lbl_8 */ 2 ), -// lbl_9: - /* load syscall argument 2 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, TCP_KEEPIDLE, /* RET_ALLOW */ 19, /* lbl_8 */ 0 ), -// lbl_8: - /* load syscall argument 1 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SOL_TCP, /* lbl_10 */ 0, /* RET_KILL_PROCESS */ 16 ), -// lbl_10: - /* load syscall argument 2 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, TCP_NODELAY, /* RET_ALLOW */ 15, /* RET_KILL_PROCESS */ 14 ), -// check_getsockopt: - /* load syscall argument 1 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SOL_TCP, /* lbl_12 */ 0, /* lbl_11 */ 2 ), -// lbl_12: - /* load syscall argument 2 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, TCP_NODELAY, /* RET_ALLOW */ 11, /* lbl_11 */ 0 ), -// lbl_11: - /* load syscall argument 1 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SOL_SOCKET, /* lbl_13 */ 0, /* RET_KILL_PROCESS */ 8 ), -// lbl_13: - /* load syscall argument 2 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SO_ERROR, /* RET_ALLOW */ 7, /* RET_KILL_PROCESS */ 6 ), -// check_shutdown: - /* load syscall argument 1 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, SHUT_WR, /* RET_ALLOW */ 5, /* RET_KILL_PROCESS */ 4 ), -// check_mmap: - /* load syscall argument 2 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, PROT_READ|PROT_WRITE, /* RET_ALLOW */ 3, /* lbl_14 */ 0 ), -// lbl_14: - /* load syscall argument 2 in accumulator */ - BPF_STMT( BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])), - BPF_JUMP( BPF_JMP | BPF_JEQ | BPF_K, PROT_NONE, /* RET_ALLOW */ 1, /* RET_KILL_PROCESS */ 0 ), -// RET_KILL_PROCESS: - /* KILL_PROCESS is placed before ALLOW since it's the fallthrough case. */ - BPF_STMT( BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS ), -// RET_ALLOW: - /* ALLOW has to be reached by jumping */ - BPF_STMT( BPF_RET | BPF_K, SECCOMP_RET_ALLOW ), - }; - fd_memcpy( out, filter, sizeof( filter ) ); -} - -#endif diff --git a/src/disco/tiles.h b/src/disco/tiles.h index 2fc2fdc53d..3c08cb1ff9 100644 --- a/src/disco/tiles.h +++ b/src/disco/tiles.h @@ -6,7 +6,7 @@ #include "../ballet/shred/fd_shred.h" #include "pack/fd_pack.h" #include "topo/fd_topo.h" -#include "plugin/fd_bundle_crank.h" +#include "bundle/fd_bundle_crank.h" #include "fd_txn_m_t.h" #include diff --git a/src/disco/topo/fd_topo.h b/src/disco/topo/fd_topo.h index 6c91f3dbb0..016910f4fd 100644 --- a/src/disco/topo/fd_topo.h +++ b/src/disco/topo/fd_topo.h @@ -191,9 +191,16 @@ typedef struct { } dedup; struct { - char url[ 256 ]; - char tls_domain_name[ 256 ]; - char identity_key_path[ PATH_MAX ]; + char url[ 256 ]; + ulong url_len; + char sni[ 256 ]; + ulong sni_len; + char identity_key_path[ PATH_MAX ]; + char key_log_path[ PATH_MAX ]; + ulong buf_sz; + ulong ssl_heap_sz; + ulong keepalive_interval_nanos; + ulong request_timeout_nanos; } bundle; struct { @@ -587,9 +594,9 @@ fd_topo_find_tile_in_link( fd_topo_t const * topo, FD_FN_PURE static inline ulong fd_topo_find_tile_out_link( fd_topo_t const * topo, - fd_topo_tile_t const * tile, - char const * name, - ulong kind_id ) { + fd_topo_tile_t const * tile, + char const * name, + ulong kind_id ) { for( ulong i=0; iout_cnt; i++ ) { if( FD_UNLIKELY( !strcmp( topo->links[ tile->out_link_id[ i ] ].name, name ) ) && topo->links[ tile->out_link_id[ i ] ].kind_id == kind_id ) return i; diff --git a/src/disco/topo/fd_topob.c b/src/disco/topo/fd_topob.c index 2ce07b9f9e..653587e597 100644 --- a/src/disco/topo/fd_topob.c +++ b/src/disco/topo/fd_topob.c @@ -64,7 +64,7 @@ fd_topob_obj( fd_topo_t * topo, return obj; } -void +fd_topo_link_t * fd_topob_link( fd_topo_t * topo, char const * link_name, char const * wksp_name, @@ -100,6 +100,8 @@ fd_topob_link( fd_topo_t * topo, FD_TEST( fd_pod_insertf_ulong( topo->props, mtu, "obj.%lu.mtu", obj->id ) ); } topo->link_cnt++; + + return link; } void diff --git a/src/disco/topo/fd_topob.h b/src/disco/topo/fd_topob.h index cbb6d100b0..162237d18d 100644 --- a/src/disco/topo/fd_topob.h +++ b/src/disco/topo/fd_topob.h @@ -72,7 +72,7 @@ fd_topob_tile_uses( fd_topo_t * topo, can have no backing data buffer, a dcache, or a reassembly buffer behind it. */ -void +fd_topo_link_t * fd_topob_link( fd_topo_t * topo, char const * link_name, char const * wksp_name, diff --git a/src/discof/poh/fd_poh_tile.c b/src/discof/poh/fd_poh_tile.c index 54cd36eb73..2aeb33db1a 100644 --- a/src/discof/poh/fd_poh_tile.c +++ b/src/discof/poh/fd_poh_tile.c @@ -309,7 +309,7 @@ #include "../bank/fd_bank_abi.h" #include "../../disco/tiles.h" -#include "../../disco/plugin/fd_bundle_crank.h" +#include "../../disco/bundle/fd_bundle_crank.h" #include "../../disco/pack/fd_pack.h" #include "../../ballet/sha256/fd_sha256.h" #include "../../disco/metrics/fd_metrics.h" diff --git a/src/discoh/poh/fd_poh_tile.c b/src/discoh/poh/fd_poh_tile.c index b6058bcde1..199dc61513 100644 --- a/src/discoh/poh/fd_poh_tile.c +++ b/src/discoh/poh/fd_poh_tile.c @@ -309,7 +309,7 @@ #include "../bank/fd_bank_abi.h" #include "../../disco/tiles.h" -#include "../../disco/plugin/fd_bundle_crank.h" +#include "../../disco/bundle/fd_bundle_crank.h" #include "../../disco/pack/fd_pack.h" #include "../../ballet/sha256/fd_sha256.h" #include "../../disco/metrics/fd_metrics.h" diff --git a/src/util/net/Local.mk b/src/util/net/Local.mk index 57b0f04019..5266ac2636 100644 --- a/src/util/net/Local.mk +++ b/src/util/net/Local.mk @@ -1,9 +1,11 @@ -$(call add-hdrs,fd_eth.h fd_ip4.h fd_igmp.h fd_pcap.h fd_pcapng.h fd_udp.h) +$(call add-hdrs,fd_eth.h fd_ip4.h fd_ip6.h fd_igmp.h fd_pcap.h fd_pcapng.h fd_udp.h) $(call add-objs,fd_eth fd_ip4 fd_pcap fd_pcapng,fd_util) $(call make-unit-test,test_eth,test_eth,fd_util) $(call run-unit-test,test_eth) $(call make-unit-test,test_ip4,test_ip4,fd_util) $(call run-unit-test,test_ip4) +$(call make-unit-test,test_ip6,test_ip6,fd_util) +$(call run-unit-test,test_ip6) $(call make-unit-test,test_igmp,test_igmp,fd_util) $(call run-unit-test,test_igmp) $(call make-unit-test,test_udp,test_udp,fd_util) diff --git a/src/util/net/fd_ip6.h b/src/util/net/fd_ip6.h new file mode 100644 index 0000000000..e07c3cf341 --- /dev/null +++ b/src/util/net/fd_ip6.h @@ -0,0 +1,34 @@ +#ifndef HEADER_fd_src_util_net_fd_ip6_h +#define HEADER_fd_src_util_net_fd_ip6_h + +#include "../bits/fd_bits.h" + +static inline void +fd_ip6_addr_ip4_mapped( uchar ip6_addr[16], + uint const ip4_addr ) { + memset( ip6_addr, 0, 10 ); + ip6_addr[ 10 ] = (uchar)0xff; + ip6_addr[ 11 ] = (uchar)0xff; + memcpy( ip6_addr+12, &ip4_addr, 4 ); +} + +static inline int +fd_ip6_addr_is_ip4_mapped( uchar const ip6_addr[16] ) { + return ( + (ip6_addr[ 0 ]==0x00) & (ip6_addr[ 1 ]==0x00) & + (ip6_addr[ 2 ]==0x00) & (ip6_addr[ 3 ]==0x00) & + (ip6_addr[ 4 ]==0x00) & (ip6_addr[ 5 ]==0x00) & + (ip6_addr[ 6 ]==0x00) & (ip6_addr[ 7 ]==0x00) & + (ip6_addr[ 8 ]==0x00) & (ip6_addr[ 9 ]==0x00) & + (ip6_addr[ 10 ]==0xff) & (ip6_addr[ 11 ]==0xff) + ); +} + +static inline uint +fd_ip6_addr_to_ip4( uchar const ip6_addr[16] ) { + uint ip4_addr; + memcpy( &ip4_addr, ip6_addr+12, 4 ); + return ip4_addr; +} + +#endif /* HEADER_fd_src_util_net_fd_ip6_h */ diff --git a/src/util/net/test_ip6.c b/src/util/net/test_ip6.c new file mode 100644 index 0000000000..58252adeb3 --- /dev/null +++ b/src/util/net/test_ip6.c @@ -0,0 +1,27 @@ +#include "fd_ip6.h" +#include "fd_ip4.h" +#include "../../util/fd_util.h" + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + + uchar ip6_addr[16]; + fd_ip6_addr_ip4_mapped( ip6_addr, FD_IP4_ADDR( 10,1,2,3 ) ); + FD_TEST( fd_memeq( ip6_addr, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x0a\x01\x02\x03", 16 ) ); + FD_TEST( fd_ip6_addr_is_ip4_mapped( ip6_addr )==1 ); + for( ulong i=0UL; i<10UL; i++ ) { + for( ulong k=0UL; k<8UL; k++ ) { + ip6_addr[ i ] = (uchar)( ip6_addr[ i ]^(1U< +#include "../h2/fd_h2_rbuf_sock.h" +#if FD_HAS_OPENSSL +#include "../openssl/fd_openssl.h" +#include +#include +#include "../h2/fd_h2_rbuf_ossl.h" +#endif + +/* Forward declarations */ + +static fd_h2_callbacks_t const fd_grpc_client_h2_callbacks; + +ulong +fd_grpc_client_align( void ) { + return alignof(fd_grpc_client_t); +} + +ulong +fd_grpc_client_footprint( void ) { + ulong l = FD_LAYOUT_INIT; + l = FD_LAYOUT_APPEND( l, alignof(fd_grpc_client_t), sizeof(fd_grpc_client_t) ); + l = FD_LAYOUT_APPEND( l, alignof(fd_grpc_client_bufs_t), sizeof(fd_grpc_client_bufs_t) ); + l = FD_LAYOUT_APPEND( l, fd_grpc_h2_stream_pool_align(), fd_grpc_h2_stream_pool_footprint( FD_GRPC_CLIENT_MAX_STREAMS ) ); + return FD_LAYOUT_FINI( l, fd_grpc_client_align() ); +} + +fd_grpc_client_t * +fd_grpc_client_new( void * mem, + fd_grpc_client_callbacks_t const * callbacks, + fd_grpc_client_metrics_t * metrics, + void * app_ctx, + ulong rng_seed ) { + if( FD_UNLIKELY( !mem ) ) { + FD_LOG_WARNING(( "NULL mem" )); + return NULL; + } + + FD_SCRATCH_ALLOC_INIT( l, mem ); + void * client_mem = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_grpc_client_t), sizeof(fd_grpc_client_t) ); + void * bufs_mem = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_grpc_client_bufs_t), sizeof(fd_grpc_client_bufs_t) ); + void * stream_pool_mem = FD_SCRATCH_ALLOC_APPEND( l, fd_grpc_h2_stream_pool_align(), fd_grpc_h2_stream_pool_footprint( FD_GRPC_CLIENT_MAX_STREAMS ) ); + FD_SCRATCH_ALLOC_FINI( l, fd_grpc_client_align() ); + + fd_grpc_client_t * client = client_mem; + fd_grpc_client_bufs_t * bufs = bufs_mem; + + fd_grpc_h2_stream_t * stream_pool = + fd_grpc_h2_stream_pool_join( fd_grpc_h2_stream_pool_new( stream_pool_mem, FD_GRPC_CLIENT_MAX_STREAMS ) ); + if( FD_UNLIKELY( !stream_pool ) ) FD_LOG_CRIT(( "Failed to create stream pool" )); /* unreachable */ + + *client = (fd_grpc_client_t){ + .callbacks = callbacks, + .ctx = app_ctx, + .stream_pool = stream_pool, + .nanopb_tx = bufs->nanopb_tx, + .frame_scratch = bufs->frame_scratch, + .metrics = metrics + }; + fd_h2_rbuf_init( client->frame_rx, bufs->frame_rx_buf, sizeof(bufs->frame_rx_buf) ); + fd_h2_rbuf_init( client->frame_tx, bufs->frame_tx_buf, sizeof(bufs->frame_tx_buf) ); + + /* FIXME for performance, cache this? */ + fd_h2_hdr_matcher_init( client->matcher, rng_seed ); + fd_h2_hdr_matcher_insert_literal( client->matcher, FD_GRPC_HDR_STATUS, "grpc-status" ); + fd_h2_hdr_matcher_insert_literal( client->matcher, FD_GRPC_HDR_MESSAGE, "grpc-message" ); + + fd_h2_conn_init_client( client->conn ); + client->conn->ctx = client; + + client->version_len = 5; + memcpy( client->version, "0.0.0", 5 ); + + /* Don't memset bufs for better performance */ + + return client; +} + +void * +fd_grpc_client_delete( fd_grpc_client_t * client ) { + return client; +} + +void +fd_grpc_client_set_version( fd_grpc_client_t * client, + char const * version, + ulong version_len ) { + if( FD_UNLIKELY( version_len > FD_GRPC_CLIENT_VERSION_LEN_MAX ) ) { + FD_LOG_WARNING(( "Version string too long (%lu chars), ignoring", version_len )); + return; + } + client->version_len = (uchar)version_len; + memcpy( client->version, version, version_len ); +} + +static void +fd_grpc_client_send_stream_quota( fd_h2_rbuf_t * rbuf_tx, + fd_grpc_h2_stream_t * stream, + uint bump ) { + fd_h2_window_update_t window_update = { + .hdr = { + .typlen = fd_h2_frame_typlen( FD_H2_FRAME_TYPE_WINDOW_UPDATE, 4UL ), + .r_stream_id = fd_uint_bswap( stream->s.stream_id ) + }, + .increment = fd_uint_bswap( bump ) + }; + fd_h2_rbuf_push( rbuf_tx, &window_update, sizeof(fd_h2_window_update_t) ); + stream->s.rx_wnd += bump; +} + +static void +fd_grpc_client_send_stream_window_updates( fd_grpc_client_t * client ) { + /* FIXME poor algorithmic inefficiency. Consider replenishing lazily + whenever data is received. */ + fd_h2_conn_t * conn = client->conn; + fd_h2_rbuf_t * rbuf_tx = client->frame_tx; + if( FD_UNLIKELY( conn->flags ) ) return; + uint const wnd_max = conn->self_settings.initial_window_size; + uint const wnd_thres = wnd_max / 2; + ulong const stream_cnt = client->stream_cnt; + for( ulong i=0UL; istreams[ i ]; + if( FD_UNLIKELY( stream->s.rx_wnd < wnd_thres ) ) { + uint const bump = wnd_max - stream->s.rx_wnd; + fd_grpc_client_send_stream_quota( rbuf_tx, stream, bump ); + } + } +} + +#if FD_HAS_OPENSSL + +static int +fd_ossl_log_error( char const * str, + ulong len, + void * ctx ) { + (void)ctx; + FD_LOG_WARNING(( "%.*s", (int)len, str )); + return 0; +} + +int +fd_grpc_client_rxtx_ossl( fd_grpc_client_t * client, + SSL * ssl, + int * charge_busy ) { + if( FD_UNLIKELY( !client->ssl_hs_done ) ) { + int res = SSL_do_handshake( ssl ); + if( res<=0 ) { + int error = SSL_get_error( ssl, res ); + if( FD_LIKELY( error==SSL_ERROR_WANT_READ || error==SSL_ERROR_WANT_WRITE ) ) return 1; + FD_LOG_WARNING(( "SSL_do_handshake failed (%i-%s)", error, fd_openssl_ssl_strerror( error ) )); + ERR_print_errors_cb( fd_ossl_log_error, NULL ); + return 0; + } else { + client->ssl_hs_done = 1; + } + } + + fd_h2_conn_t * conn = client->conn; + int ssl_err = 0; + ulong read_sz = fd_h2_rbuf_ssl_read( client->frame_rx, ssl, &ssl_err ); + if( FD_UNLIKELY( ssl_err && ssl_err!=SSL_ERROR_WANT_READ ) ) { + if( ssl_err==SSL_ERROR_ZERO_RETURN ) { + FD_LOG_WARNING(( "gRPC server closed connection" )); + return 0; + } + FD_LOG_WARNING(( "SSL_read_ex failed (%i-%s)", ssl_err, fd_openssl_ssl_strerror( ssl_err ) )); + ERR_print_errors_cb( fd_ossl_log_error, NULL ); + return 0; + } + if( FD_UNLIKELY( conn->flags ) ) fd_h2_tx_control( conn, client->frame_tx, &fd_grpc_client_h2_callbacks ); + fd_h2_rx( conn, client->frame_rx, client->frame_tx, client->frame_scratch, FD_GRPC_CLIENT_BUFSZ, &fd_grpc_client_h2_callbacks ); + fd_grpc_client_send_stream_window_updates( client ); + ulong write_sz = fd_h2_rbuf_ssl_write( client->frame_tx, ssl ); + + if( read_sz!=0 || write_sz!=0 ) *charge_busy = 1; + return 1; +} + +#endif /* FD_HAS_OPENSSL */ + +#if FD_H2_HAS_SOCKETS + +int +fd_grpc_client_rxtx_socket( fd_grpc_client_t * client, + int sock_fd, + int * charge_busy ) { + fd_h2_conn_t * conn = client->conn; + + ulong frame_rx_0 = client->frame_rx->hi_off; + int rx_err = fd_h2_rbuf_recvmsg( client->frame_rx, sock_fd, MSG_NOSIGNAL|MSG_DONTWAIT ); + if( FD_UNLIKELY( rx_err ) ) { + FD_LOG_INFO(( "Disconnected: recvmsg error (%i-%s)", rx_err, fd_io_strerror( rx_err ) )); + return 0; + } + ulong frame_rx_1 = client->frame_rx->hi_off; + if( frame_rx_0!=frame_rx_1 ) *charge_busy = 1; + + if( FD_UNLIKELY( conn->flags ) ) fd_h2_tx_control( conn, client->frame_tx, &fd_grpc_client_h2_callbacks ); + fd_h2_rx( conn, client->frame_rx, client->frame_tx, client->frame_scratch, FD_GRPC_CLIENT_BUFSZ, &fd_grpc_client_h2_callbacks ); + fd_grpc_client_send_stream_window_updates( client ); + + ulong frame_tx_0 = client->frame_tx->lo_off; + int tx_err = fd_h2_rbuf_sendmsg( client->frame_tx, sock_fd, MSG_NOSIGNAL|MSG_DONTWAIT ); + if( FD_UNLIKELY( tx_err ) ) { + FD_LOG_WARNING(( "fd_h2_rbuf_sendmsg failed (%i-%s)", tx_err, fd_io_strerror( tx_err ) )); + return 0; + } + ulong frame_tx_1 = client->frame_tx->lo_off; + + if( frame_rx_0!=frame_rx_1 || frame_tx_0!=frame_tx_1 ) *charge_busy = 1; + return 1; +} + +#endif /* FD_H2_HAS_SOCKETS */ + +/* fd_grpc_client_request continue attempts to write a request data + frame. */ + +static int +fd_grpc_client_request_continue1( fd_grpc_client_t * client ) { + fd_grpc_h2_stream_t * stream = client->request_stream; + fd_h2_stream_t * h2_stream = &stream->s; + fd_h2_tx_op_copy( client->conn, h2_stream, client->frame_tx, client->request_tx_op ); + if( FD_UNLIKELY( client->request_tx_op->chunk_sz ) ) return 0; + if( FD_UNLIKELY( h2_stream->state != FD_H2_STREAM_STATE_CLOSING_TX ) ) return 0; + client->metrics->stream_chunks_tx_cnt++; + /* Request finished */ + client->request_stream = NULL; + client->callbacks->tx_complete( client->ctx, stream->request_ctx ); + return 1; +} + +static int +fd_grpc_client_request_continue( fd_grpc_client_t * client ) { + if( FD_UNLIKELY( client->conn->flags & FD_H2_CONN_FLAGS_DEAD ) ) return 0; + if( FD_UNLIKELY( !client->request_stream ) ) return 0; + if( FD_UNLIKELY( !client->request_tx_op->chunk_sz ) ) return 0; + return fd_grpc_client_request_continue1( client ); +} + +/* fd_grpc_client_stream_acquire grabs a new stream ID and a stream + object. */ + +static inline int +fd_grpc_client_stream_acquire_is_safe( fd_grpc_client_t * client ) { + /* Sufficient quota to start a stream? */ + if( FD_UNLIKELY( client->conn->stream_active_cnt[1]+1 > client->conn->peer_settings.max_concurrent_streams ) ) { + FD_LOG_ERR(( "out of quota" )); + return 0; + } + + /* Free stream object available? */ + if( FD_UNLIKELY( !fd_grpc_h2_stream_pool_free( client->stream_pool ) ) ) { + FD_LOG_ERR(( "nothing free %u", client->conn->stream_active_cnt[1] )); + return 0; + } + if( FD_UNLIKELY( client->stream_cnt >= FD_GRPC_CLIENT_MAX_STREAMS ) ) { + FD_LOG_ERR(( "nothing free 2 %lu", client->stream_cnt )); + return 0; + } + + return 1; +} + +static fd_grpc_h2_stream_t * +fd_grpc_client_stream_acquire( fd_grpc_client_t * client, + ulong request_ctx ) { + if( FD_UNLIKELY( client->stream_cnt >= FD_GRPC_CLIENT_MAX_STREAMS ) ) { + FD_LOG_CRIT(( "stream pool exhausted" )); + } + + fd_h2_conn_t * conn = client->conn; + uint const stream_id = client->conn->tx_stream_next; + conn->tx_stream_next += 2U; + + fd_grpc_h2_stream_t * stream = fd_grpc_h2_stream_pool_ele_acquire( client->stream_pool ); + stream->request_ctx = request_ctx; + memset( &stream->hdrs, 0, sizeof(stream->hdrs) ); + stream->hdrs.grpc_status = FD_GRPC_STATUS_UNKNOWN; + stream->hdrs_received = 0; + stream->msg_buf_used = 0; + stream->msg_sz = 0; + + fd_h2_stream_open( fd_h2_stream_init( &stream->s ), conn, stream_id ); + client->request_stream = stream; + client->stream_ids[ client->stream_cnt ] = stream_id; + client->streams [ client->stream_cnt ] = stream; + client->stream_cnt++; + + return stream; +} + +static void +fd_grpc_client_stream_release( fd_grpc_client_t * client, + fd_grpc_h2_stream_t * stream ) { + if( FD_UNLIKELY( !client->stream_cnt ) ) FD_LOG_CRIT(( "stream map corrupt" )); /* unreachable */ + + /* Deallocate tx_op */ + if( FD_UNLIKELY( stream == client->request_stream ) ) { + client->request_stream = NULL; + *client->request_tx_op = (fd_h2_tx_op_t){0}; + } + + /* Remove stream from map */ + int map_idx = -1; + for( int i=0UL; istream_ids[ i ] == stream->s.stream_id ) { + map_idx = i; + } + } + if( FD_UNLIKELY( map_idx<0 ) ) FD_LOG_CRIT(( "stream map corrupt" )); /* unreachable */ + if( (ulong)map_idx+1 < client->stream_cnt ) { + client->stream_ids[ map_idx ] = client->stream_ids[ client->stream_cnt-1 ]; + client->streams [ map_idx ] = client->streams [ client->stream_cnt-1 ]; + } + client->stream_cnt--; + + fd_grpc_h2_stream_pool_ele_release( client->stream_pool, stream ); +} + +int +fd_grpc_client_request_is_blocked( fd_grpc_client_t * client ) { + if( FD_UNLIKELY( !client ) ) return 1; + if( FD_UNLIKELY( client->conn->flags & FD_H2_CONN_FLAGS_DEAD ) ) return 1; + if( FD_UNLIKELY( !client->h2_hs_done ) ) return 1; + if( FD_UNLIKELY( !fd_h2_rbuf_is_empty( client->frame_tx ) ) ) return 1; + if( FD_UNLIKELY( !fd_grpc_client_stream_acquire_is_safe( client ) ) ) return 1; + return 0; +} + +int +fd_grpc_client_request_start( + fd_grpc_client_t * client, + char const * host, + ulong host_len, + ushort port, + char const * path, + ulong path_len, + ulong request_ctx, + pb_msgdesc_t const * fields, + void const * message, + char const * auth_token, + ulong auth_token_sz +) { + if( FD_UNLIKELY( fd_grpc_client_request_is_blocked( client ) ) ) return 0; + + /* Encode message */ + FD_STATIC_ASSERT( sizeof(((fd_grpc_client_bufs_t *)0)->nanopb_tx) >= FD_GRPC_CLIENT_MSG_SZ_MAX, sz ); + uchar * proto_buf = client->nanopb_tx + sizeof(fd_grpc_hdr_t); + pb_ostream_t ostream = pb_ostream_from_buffer( proto_buf, FD_GRPC_CLIENT_MSG_SZ_MAX ); + if( FD_UNLIKELY( !pb_encode( &ostream, fields, message ) ) ) { + FD_LOG_WARNING(( "Failed to encode Protobuf message (%.*s). This is a bug (insufficient buffer space?)", (int)path_len, path )); + return 0; + } + ulong const serialized_sz = ostream.bytes_written; + + /* Create gRPC length prefix */ + fd_grpc_hdr_t hdr = { + .compressed=0, + .msg_sz=fd_uint_bswap( (uint)serialized_sz ) + }; + memcpy( client->nanopb_tx, &hdr, sizeof(fd_grpc_hdr_t) ); + ulong const payload_sz = serialized_sz + sizeof(fd_grpc_hdr_t); + + /* Allocate stream descriptor */ + fd_grpc_h2_stream_t * stream = fd_grpc_client_stream_acquire( client, request_ctx ); + uint const stream_id = stream->s.stream_id; + + /* Write HTTP/2 request headers */ + fd_h2_tx_prepare( client->conn, client->frame_tx, FD_H2_FRAME_TYPE_HEADERS, FD_H2_FLAG_END_HEADERS, stream_id ); + fd_grpc_req_hdrs_t req_meta = { + .host = host, + .host_len = host_len, + .port = port, + .path = path, + .path_len = path_len, + .https = 1, /* grpc_client assumes TLS encryption for now */ + + .bearer_auth = auth_token, + .bearer_auth_len = auth_token_sz + }; + if( FD_UNLIKELY( !fd_grpc_h2_gen_request_hdrs( + &req_meta, + client->frame_tx, + client->version, + client->version_len + ) ) ) { + FD_LOG_WARNING(( "Failed to generate gRPC request headers (%.*s). This is a bug", (int)path_len, path )); + return 0; + } + fd_h2_tx_commit( client->conn, client->frame_tx ); + + /* Queue request payload for send + (Protobuf message might have to be fragmented into multiple HTTP/2 + DATA frames if the client gets blocked) */ + fd_h2_tx_op_init( client->request_tx_op, client->nanopb_tx, payload_sz, FD_H2_FLAG_END_STREAM ); + fd_grpc_client_request_continue1( client ); + client->metrics->requests_sent++; + client->metrics->streams_active++; + + FD_LOG_DEBUG(( "gRPC request path=%.*s sz=%lu", (int)path_len, path, serialized_sz )); + + return 1; +} + +/* Lookup stream by ID */ + +static fd_h2_stream_t * +fd_grpc_h2_stream_query( fd_h2_conn_t * conn, + uint stream_id ) { + fd_grpc_client_t * client = conn->ctx; + for( ulong i=0UL; istream_ids[ i ] == stream_id ) { + return &client->streams[ i ]->s; + } + } + return NULL; +} + +static void +fd_grpc_h2_conn_established( fd_h2_conn_t * conn ) { + fd_grpc_client_t * client = conn->ctx; + client->h2_hs_done = 1; + client->callbacks->conn_established( client->ctx ); +} + +static void +fd_grpc_h2_conn_final( fd_h2_conn_t * conn, + uint h2_err, + int closed_by ) { + fd_grpc_client_t * client = conn->ctx; + client->callbacks->conn_dead( client->ctx, h2_err, closed_by ); +} + +/* React to response data */ + +static void +fd_grpc_h2_cb_headers( + fd_h2_conn_t * conn, + fd_h2_stream_t * h2_stream, + void const * data, + ulong data_sz, + ulong flags +) { + fd_grpc_h2_stream_t * stream = fd_grpc_h2_stream_upcast( h2_stream ); + fd_grpc_client_t * client = conn->ctx; + + int h2_status = fd_grpc_h2_read_response_hdrs( &stream->hdrs, client->matcher, data, data_sz ); + if( FD_UNLIKELY( h2_status!=FD_H2_SUCCESS ) ) { + /* Failed to parse HTTP/2 headers */ + fd_h2_stream_error( h2_stream, client->frame_tx, FD_H2_ERR_PROTOCOL ); + client->callbacks->rx_end( client->ctx, stream->request_ctx, &stream->hdrs ); /* invalidates stream->hdrs */ + fd_grpc_client_stream_release( client, stream ); + } + + if( !stream->hdrs_received && !!( flags & FD_H2_FLAG_END_HEADERS) ) { + /* Got initial response header */ + stream->hdrs_received = 1; + if( FD_LIKELY( ( stream->hdrs.h2_status==200 ) & + ( !!stream->hdrs.is_grpc_proto ) ) ) { + client->callbacks->rx_start( client->ctx, stream->request_ctx ); + } + } + + if( ( flags & (FD_H2_FLAG_END_HEADERS|FD_H2_FLAG_END_STREAM) ) + ==(FD_H2_FLAG_END_HEADERS|FD_H2_FLAG_END_STREAM) ) { + client->callbacks->rx_end( client->ctx, stream->request_ctx, &stream->hdrs ); + fd_grpc_client_stream_release( client, stream ); + } +} + +static void +fd_grpc_h2_cb_data( + fd_h2_conn_t * conn, + fd_h2_stream_t * h2_stream, + void const * data, + ulong data_sz, + ulong flags +) { + fd_grpc_client_t * client = conn->ctx; + fd_grpc_h2_stream_t * stream = fd_grpc_h2_stream_upcast( h2_stream ); + if( FD_UNLIKELY( ( stream->hdrs.h2_status!=200 ) | + ( !stream->hdrs.is_grpc_proto ) ) ) { + return; + } + + do { + + /* Read header bytes */ + if( stream->msg_buf_used < sizeof(fd_grpc_hdr_t) ) { + ulong hdr_frag_sz = fd_ulong_min( sizeof(fd_grpc_hdr_t) - stream->msg_buf_used, data_sz ); + fd_memcpy( stream->msg_buf + stream->msg_buf_used, data, hdr_frag_sz ); + stream->msg_buf_used += hdr_frag_sz; + data = (void const *)( (ulong)data + (ulong)hdr_frag_sz ); + data_sz -= hdr_frag_sz; + if( FD_UNLIKELY( stream->msg_buf_used < sizeof(fd_grpc_hdr_t) ) ) return; + + /* Header complete */ + stream->msg_sz = fd_uint_bswap( FD_LOAD( uint, (void *)( (ulong)stream->msg_buf+1 ) ) ); + if( FD_UNLIKELY( stream->msg_sz > FD_GRPC_CLIENT_MSG_SZ_MAX ) ) { + FD_LOG_WARNING(( "Received oversized gRPC message (%lu bytes), killing request", stream->msg_sz )); + fd_h2_stream_error( h2_stream, client->frame_tx, FD_H2_ERR_INTERNAL ); + fd_grpc_client_stream_release( client, stream ); + return; + } + } + + /* Read payload bytes */ + ulong wmark = sizeof(fd_grpc_hdr_t) + stream->msg_sz; + ulong chunk_sz = fd_ulong_min( stream->msg_buf_used+data_sz, wmark ) - stream->msg_buf_used; + if( FD_UNLIKELY( chunk_sz>data_sz ) ) FD_LOG_CRIT(( "integer underflow" )); /* unreachable */ + fd_memcpy( stream->msg_buf + stream->msg_buf_used, data, chunk_sz ); + stream->msg_buf_used += chunk_sz; + data = (void const *)( (ulong)data + (ulong)chunk_sz ); + data_sz -= chunk_sz; + + client->metrics->stream_chunks_rx_cnt++; + client->metrics->stream_chunks_rx_bytes += chunk_sz; + + if( stream->msg_buf_used >= wmark ) { + /* Data complete */ + void const * msg_ptr = stream->msg_buf + sizeof(fd_grpc_hdr_t); + client->callbacks->rx_msg( client->ctx, msg_ptr, stream->msg_sz, stream->request_ctx ); + stream->msg_buf_used = 0UL; + stream->msg_sz = 0UL; + } + + } while( data_sz ); + + if( flags & FD_H2_FLAG_END_STREAM ) { + /* FIXME incomplete gRPC message */ + if( FD_UNLIKELY( !stream->msg_buf_used ) ) { + FD_LOG_WARNING(( "Received incomplete gRPC message" )); + } + client->callbacks->rx_end( client->ctx, stream->request_ctx, &stream->hdrs ); + } +} + +/* Server might kill our request */ + +static void +fd_grpc_h2_rst_stream( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + uint error_code, + int closed_by ) { + if( closed_by==1 ) { + FD_LOG_WARNING(( "Server terminated request stream_id=%u (%u-%s)", + stream->stream_id, error_code, fd_h2_strerror( error_code ) )); + } else { + FD_LOG_WARNING(( "Stream failed stream_id=%u (%u-%s)", + stream->stream_id, error_code, fd_h2_strerror( error_code ) )); + } + fd_grpc_client_t * client = conn->ctx; + fd_grpc_client_stream_release( client, fd_grpc_h2_stream_upcast( stream ) ); +} + +/* A HTTP/2 flow control change might unblock a queued request send op */ + +void +fd_grpc_h2_window_update( fd_h2_conn_t * conn, + uint increment ) { + (void)increment; + fd_grpc_client_request_continue( conn->ctx ); +} + +void +fd_grpc_h2_stream_window_update( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + uint increment ) { + (void)stream; (void)increment; + fd_grpc_client_request_continue( conn->ctx ); +} + +void +fd_grpc_h2_ping_ack( fd_h2_conn_t * conn ) { + fd_grpc_client_t * client = conn->ctx; + client->callbacks->ping_ack( client->ctx ); +} + +fd_h2_rbuf_t * +fd_grpc_client_rbuf_tx( fd_grpc_client_t * client ) { + return client->frame_tx; +} + +fd_h2_rbuf_t * +fd_grpc_client_rbuf_rx( fd_grpc_client_t * client ) { + return client->frame_rx; +} + +fd_h2_conn_t * +fd_grpc_client_h2_conn( fd_grpc_client_t * client ) { + return client->conn; +} + +/* fd_grpc_client_h2_callbacks specifies h2->grpc_client callbacks. + Stored in .rodata for security. Must be kept in sync with fd_h2 to + avoid NULL pointers. */ + +static fd_h2_callbacks_t const fd_grpc_client_h2_callbacks = { + .stream_create = fd_h2_noop_stream_create, + .stream_query = fd_grpc_h2_stream_query, + .conn_established = fd_grpc_h2_conn_established, + .conn_final = fd_grpc_h2_conn_final, + .headers = fd_grpc_h2_cb_headers, + .data = fd_grpc_h2_cb_data, + .rst_stream = fd_grpc_h2_rst_stream, + .window_update = fd_grpc_h2_window_update, + .stream_window_update = fd_grpc_h2_stream_window_update, + .ping_ack = fd_grpc_h2_ping_ack, +}; diff --git a/src/waltz/grpc/fd_grpc_client.h b/src/waltz/grpc/fd_grpc_client.h new file mode 100644 index 0000000000..79db8deaba --- /dev/null +++ b/src/waltz/grpc/fd_grpc_client.h @@ -0,0 +1,280 @@ +#ifndef HEADER_fd_src_waltz_grpc_fd_grpc_client_h +#define HEADER_fd_src_waltz_grpc_fd_grpc_client_h + +/* fd_grpc_client.h provides an API for dispatching unary and server- + streaming gRPC requests over HTTP/2+TLS. */ + +#include "fd_grpc_codec.h" +#include "../../ballet/nanopb/pb_firedancer.h" /* pb_msgdesc_t */ +#if FD_HAS_OPENSSL +#include /* SSL */ +#endif + +struct fd_grpc_client_private; +typedef struct fd_grpc_client_private fd_grpc_client_t; + +/* FIXME don't hardcode these limits */ + +/* FD_GRPC_CLIENT_MSG_SZ_MAX is the largest serialized Protobuf + message that grpc_client can handle. + + FD_GRPC_CLIENT_BUFSZ, which sets the size of frame buffers wrapping + messages, is at least FD_GRPC_CLIENT_REQUEST_SZ_MAX bytes large, + plus FD_GRPC_CLIENT_BUFFER_SLACK bytes of "slack" for headers. + + FIXME consider making these dynamic, and move them to the config file */ + +#define FD_GRPC_CLIENT_MSG_SZ_MAX (1<<16) /* 64 KiB */ +#define FD_GRPC_CLIENT_BUFFER_SLACK (1<<12) /* 4 KiB */ +#define FD_GRPC_CLIENT_BUFSZ (FD_GRPC_CLIENT_MSG_SZ_MAX+FD_GRPC_CLIENT_BUFFER_SLACK) + +/* FD_GRPC_CLIENT_MAX_STREAMS specifies the max number of inflight + unary and server-streaming requests. Note that grpc_client does + not scale well to large numbers due to O(n) algorithms. */ + +#define FD_GRPC_CLIENT_MAX_STREAMS 8 + +/* fd_grpc_client_metrics_t hold counters that are incremented by a + grpc_client. */ + +struct fd_grpc_client_metrics { + + /* wakeup_cnt counts the number of times the gRPC client was polled + for I/O. */ + ulong wakeup_cnt; + + /* stream_err_cnt counts the number of survivable stream errors. + These include out-of-memory conditions and decode failures. */ + ulong stream_err_cnt; + + /* conn_err_cnt counts the number of connection errors that resulted + in connection termination. These include protocol and I/O errors. */ + ulong conn_err_cnt; + + /* stream_chunks_tx_cnt increments whenever a DATA frame containing + request bytes is sent. stream_chunks_tx_bytes counts the number of + stream bytes sent. */ + ulong stream_chunks_tx_cnt; + ulong stream_chunks_tx_bytes; + + /* stream_chunks_rx_cnt increments whenever a DATA frame containing + response bytes is received. stream_chunks_rx_bytes counts the + number of stream bytes received. */ + ulong stream_chunks_rx_cnt; + ulong stream_chunks_rx_bytes; + + /* requests_sent increments whenever a gRPC request finished sending. */ + ulong requests_sent; + + /* streams_active is the number of streams not in 'closed' state. */ + long streams_active; + + /* rx_wait_ticks_cum is the cumulative time in ticks that incoming + gRPC messages were in a "waiting" state. The waiting state begins + when the first byte of a HTTP/2 frame is received, and ends when + all gRPC message bytes are received. + + This is a rough measure of server-to-client congestion. On a + healthy connection, this value should be close to zero. */ + long rx_wait_ticks_cum; + + /* tx_wait_ticks_cum is the cumulative time in ticks that an outgoing + message was in a "waiting" state. The waiting state begins when + a message is ready to be sent, and ends when all message bytes were + handed to the TCP layer. + + This is a rough measure of client-to-server congestion, which can + be caused by the TCP server receive window, TCP client congestion + control, or HTTP/2 server flow control. On a healthy connection, + this value should be close to zero. */ + long tx_wait_ticks_cum; + +}; + +typedef struct fd_grpc_client_metrics fd_grpc_client_metrics_t; + +/* fd_grpc_client_callbacks_t is a virtual function table containing + grpc_client->app callbacks. */ + +struct fd_grpc_client_callbacks { + + /* conn_established is called when the initial HTTP/2 SETTINGS + exchange concludes. Technically, requests can be sent before this + point, though. */ + + void + (* conn_established)( void * app_ctx ); + + /* conn_dead is called when the HTTP/2 connection ends. This state is + not recoverable and the grpc_client should be recreated. */ + + void + (* conn_dead)( void * app_ctx, + uint h2_err, + int closed_by ); + + /* tx_complete marks the completion of a tx operation. */ + + void + (* tx_complete)( void * app_ctx, + ulong request_ctx ); + + /* rx_start signals that the server sent back a response header + indicating success. rx_start is always called before the first + call to rx_msg for that request_ctx. */ + + void + (* rx_start)( void * app_ctx, + ulong request_ctx ); + + /* rx_msg delivers a gRPC message. May be called multiple times for + the same request (server streaming). */ + + void + (* rx_msg)( void * app_ctx, + void const * protobuf, + ulong protobuf_sz, + ulong request_ctx ); + + /* rx_end indicates that no more rx_msg callbacks will be delivered + for a request. */ + + void + (* rx_end)( void * app_ctx, + ulong request_ctx, + fd_grpc_resp_hdrs_t * resp ); + + /* ping_ack delivers an acknowledgement of a PING that was previously + sent by fd_h2_tx_ping. */ + + void + (* ping_ack)( void * app_ctx ); + +}; + +typedef struct fd_grpc_client_callbacks fd_grpc_client_callbacks_t; + +FD_PROTOTYPES_BEGIN + +/* Constructors */ + +ulong +fd_grpc_client_align( void ); + +ulong +fd_grpc_client_footprint( void ); + +fd_grpc_client_t * +fd_grpc_client_new( void * mem, + fd_grpc_client_callbacks_t const * callbacks, + fd_grpc_client_metrics_t * metrics, + void * app_ctx, + ulong rng_seed ); + +void * +fd_grpc_client_delete( fd_grpc_client_t * client ); + +/* fd_grpc_client_set_version sets the gRPC client's version string + (relayed via user-agent header). No reference to the provided string + is kept (the content is copied out to the client object). version + does not have to be null-terminated. version_len must be + FD_GRPC_CLIENT_VERSION_LEN_MAX or less, otherwise a warning is logged + and the client's version string remains unchanged. */ + +#define FD_GRPC_CLIENT_VERSION_LEN_MAX (63UL) + +void +fd_grpc_client_set_version( fd_grpc_client_t * client, + char const * version, + ulong version_len ); + +#if FD_HAS_OPENSSL + +/* fd_grpc_client_rxtx_ossl drives I/O against the SSL object + (SSL_read_ex and SSL_write_ex). + + This function currently copies back-and-forth between SSL and + fd_h2 rbuf. This could be improved by adding an interface to allow + OpenSSL->h2 or h2->OpenSSL writes to directly place data into the + target buffer. + + Returns 1 on success and 0 if there is an unrecoverable SSL error. */ + +int +fd_grpc_client_rxtx_ossl( fd_grpc_client_t * client, + SSL * ssl, + int * charge_busy ); + +#endif /* FD_HAS_OPENSSL */ + +/* fd_grpc_client_rxtx_socket drives I/O against a TCP socket. + (recvmsg(2) and sendmsg(2)). Uses MSG_NOSIGNAL|MSG_DONTWAIT flags. */ + +int +fd_grpc_client_rxtx_socket( fd_grpc_client_t * client, + int sock_fd, + int * charge_busy ); + +/* fd_grpc_client_request_start queues a gRPC request for send. The + request includes one Protobuf message (unary request). The client + can only write one request payload at a time, but can have multiple + requests pending for responses. + + path is the HTTP request path which usually follows the pattern + '/path.to.package/Service.Function'. If auth_token_sz is greater + than zero, adds a request header 'authorization: Bearer *auth_token'. + + request_ctx is an arbitrary number used to identify the request. It + echoes in callbacks. + + fields points to a generated nanopb descriptor. message points to a + generated nanopb struct that the user filled in with info. Calls + pb_encode() internally. + + auth_token is an optional authorization header. The header value is + prepended with "Bearer ". auth_token_sz==0 omits the auth header. + + Conditions for starting send: + - The connection is not dead and the HTTP/2 handshake is complete. + - Client has quota to open a new stream (MAX_CONCURRENT_STREAMS) + - There is no other request still sending. + - The message serialized size does not exceed + FD_GRPC_CLIENT_REQUEST_SZ_MAX. + - rbuf_tx is empty. (HTTP/2 frames all flushed out to sockets) */ + +int +fd_grpc_client_request_start( + fd_grpc_client_t * client, + char const * host, /* FIXME consider moving host:port to new() */ + ulong host_len, + ushort port, + char const * path, + ulong path_len, /* in [0,128) */ + ulong request_ctx, + pb_msgdesc_t const * fields, + void const * message, + char const * auth_token, + ulong auth_token_sz +); + +/* fd_grpc_client_request_is_blocked returns 1 if a call to + fd_grpc_client_request_start would certainly fail. Reasons include + SSL / HTTP/2 handshake not complete, or buffers blocked. */ + +int +fd_grpc_client_request_is_blocked( fd_grpc_client_t * client ); + +/* Pointers to internals for testing */ + +fd_h2_rbuf_t * +fd_grpc_client_rbuf_tx( fd_grpc_client_t * client ); + +fd_h2_rbuf_t * +fd_grpc_client_rbuf_rx( fd_grpc_client_t * client ); + +fd_h2_conn_t * +fd_grpc_client_h2_conn( fd_grpc_client_t * client ); + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_waltz_grpc_fd_grpc_client_h */ diff --git a/src/waltz/grpc/fd_grpc_client_private.h b/src/waltz/grpc/fd_grpc_client_private.h new file mode 100644 index 0000000000..e439f3ee9f --- /dev/null +++ b/src/waltz/grpc/fd_grpc_client_private.h @@ -0,0 +1,143 @@ +#ifndef HEADER_fd_src_waltz_grpc_fd_grpc_client_private_h +#define HEADER_fd_src_waltz_grpc_fd_grpc_client_private_h + +#include "fd_grpc_client.h" +#include "../grpc/fd_grpc_codec.h" +#include "../h2/fd_h2.h" + +/* fd_grpc_h2_stream_t holds the state of a gRPC request. */ + +struct fd_grpc_h2_stream { + fd_h2_stream_t s; + + ulong request_ctx; + uint next; + + /* Buffer response headers */ + fd_grpc_resp_hdrs_t hdrs; + + /* Buffer an incoming gRPC message */ + uchar msg_buf[ sizeof(fd_grpc_hdr_t)+FD_GRPC_CLIENT_MSG_SZ_MAX ]; + uint hdrs_received : 1; + ulong msg_buf_used; /* including header */ + ulong msg_sz; /* size of next message */ +}; + +typedef struct fd_grpc_h2_stream fd_grpc_h2_stream_t; + +/* Declare a pool of stream objects. + + While only one stream is used to write requests out to the wire at a + time, a gRPC client might be waiting for multiple responses. */ + +#define POOL_NAME fd_grpc_h2_stream_pool +#define POOL_T fd_grpc_h2_stream_t +#define POOL_IDX_T uint +#include "../../util/tmpl/fd_pool.c" + +static inline fd_grpc_h2_stream_t * +fd_grpc_h2_stream_upcast( fd_h2_stream_t * stream ) { + return (fd_grpc_h2_stream_t *)( (ulong)stream - offsetof(fd_grpc_h2_stream_t, s) ); +} + +/* I/O paths + + RX path + - fd_grpc_client_rxtx + - calls fd_h2_rbuf_ssl_read + - calls SSL_read_ex + - calls recv(2) + + TX path + - fd_grpc_client_rxtx + - calls fd_h2_rbuf_ssl_write + - calls SSL_write_ex + - calls send(2) */ + +#include "../../waltz/h2/fd_h2_rbuf_ossl.h" + +/* gRPC client internal state. Quick overview: + + - The client maintains exactly one gRPC connection. + - This conn includes a TCP socket, SSL handle, and a fd_h2 conn + (and accompanying buffers). + - The client object dies when the connection dies. + + - The client manages a small pool of stream objects. + - Each stream has one of 3 states: + - IDLE: marked free in stream_pool + - OPEN: sending request data. marked used in stream_pool, present + in stream_ids/streams, referred to by request_stream, has + associated tx_op object. + - CLOSE_TX: request sent, waiting for response. marked used in + stream_pool, present in stream_ids/streams. + - Only 1 stream can be in OPEN state. + + Regular state transitions: + + - IDLE->OPEN: Client acquires a stream object and starts a tx_op + See fd_grpc_client_request_start + - OPEN->CLOSE_TX: tx_op finished writing request data and is now + waiting for the response. tx_op object finalized. + See fd_grpc_client_request_continue1 + - CLOSE_TX->IDLE: All response data arrived. Stream object + deallocated. + + Irregular state transitions: + + - CLOSE_TX->IDLE: Server aborts stream before request is fully sent. + - OPEN->IDLE: Server aborts stream before response is received. */ + +struct fd_grpc_client_private { + fd_grpc_client_callbacks_t const * callbacks; + void * ctx; + + fd_h2_hdr_matcher_t matcher[1]; + + /* HTTP/2 connection */ + fd_h2_conn_t conn[1]; + fd_h2_rbuf_t frame_rx[1]; /* unencrypted HTTP/2 RX frame buffer */ + fd_h2_rbuf_t frame_tx[1]; /* unencrypted HTTP/2 TX frame buffer */ + + /* TLS connection */ + uint ssl_hs_done : 1; + uint h2_hs_done : 1; + + /* Inflight request + Non-NULL until a gRPC request is fully written out. */ + fd_grpc_h2_stream_t * request_stream; + fd_h2_tx_op_t request_tx_op[1]; + + /* Stream pool */ + fd_grpc_h2_stream_t * stream_pool; + + /* Stream map */ + /* FIXME pull this into a fd_map_tiny.c? */ + uint stream_ids[ FD_GRPC_CLIENT_MAX_STREAMS ]; + fd_grpc_h2_stream_t * streams [ FD_GRPC_CLIENT_MAX_STREAMS ]; + ulong stream_cnt; + + /* Buffers */ + uchar * nanopb_tx; + uchar * frame_scratch; + + /* Version string */ + uchar version_len; + char version[ FD_GRPC_CLIENT_VERSION_LEN_MAX ]; + + fd_grpc_client_metrics_t * metrics; +}; + +struct fd_grpc_client_bufs { + /* Nanopb serialize buffer */ + uchar nanopb_tx[ FD_GRPC_CLIENT_MSG_SZ_MAX ]; + + /* Frame buffers */ + uchar frame_rx_buf[ FD_GRPC_CLIENT_BUFSZ ]; + uchar frame_tx_buf[ FD_GRPC_CLIENT_BUFSZ ]; + uchar frame_scratch[ FD_GRPC_CLIENT_BUFSZ ]; +}; + +typedef struct fd_grpc_client_bufs fd_grpc_client_bufs_t; + +#endif /* HEADER_fd_src_waltz_grpc_fd_grpc_client_private_h */ diff --git a/src/waltz/grpc/fd_grpc_codec.c b/src/waltz/grpc/fd_grpc_codec.c new file mode 100644 index 0000000000..2fe7ff0413 --- /dev/null +++ b/src/waltz/grpc/fd_grpc_codec.c @@ -0,0 +1,112 @@ +#include "fd_grpc_codec.h" +#include "../h2/fd_hpack.h" +#include "../h2/fd_hpack_wr.h" + +static int +fd_hpack_wr_content_type_grpc( fd_h2_rbuf_t * rbuf_tx ) { + static char const code[] = + "\x5f" "\x16" "application/grpc+proto"; + if( FD_UNLIKELY( fd_h2_rbuf_free_sz( rbuf_tx)path, req->path_len ) ) ) return 0; + if( FD_UNLIKELY( !fd_hpack_wr_authority( rbuf_tx, req->host, req->host_len, req->port ) ) ) return 0; + if( FD_UNLIKELY( !fd_hpack_wr_trailers( rbuf_tx ) ) ) return 0; + if( FD_UNLIKELY( !fd_hpack_wr_content_type_grpc( rbuf_tx ) ) ) return 0; + + static char const user_agent[] = "grpc-firedancer/"; + ulong const user_agent_len = sizeof(user_agent)-1 + version_len; + if( FD_UNLIKELY( !fd_hpack_wr_user_agent( rbuf_tx, user_agent_len ) ) ) return 0; + fd_h2_rbuf_push( rbuf_tx, user_agent, sizeof(user_agent)-1 ); + fd_h2_rbuf_push( rbuf_tx, version, version_len ); + + if( req->bearer_auth_len ) { + if( FD_UNLIKELY( !fd_hpack_wr_auth_bearer( rbuf_tx, req->bearer_auth, req->bearer_auth_len ) ) ) return 0; + } + return 1; +} + +/* fd_grpc_h2_parse_num parses a decimal number in [1,999]. */ + +static uint +fd_grpc_h2_parse_num( char const * num, + ulong num_len ) { + num_len = fd_ulong_min( num_len, 10 ); + char num_cstr[ 11 ]; + fd_cstr_fini( fd_cstr_append_text( fd_cstr_init( num_cstr ), num, num_len ) ); + return fd_cstr_to_uint( num_cstr ); +} + +int +fd_grpc_h2_read_response_hdrs( fd_grpc_resp_hdrs_t * resp, + fd_h2_hdr_matcher_t const * matcher, + uchar const * payload, + ulong payload_sz ) { + fd_hpack_rd_t hpack_rd[1]; + fd_hpack_rd_init( hpack_rd, payload, payload_sz ); + while( !fd_hpack_rd_done( hpack_rd ) ) { + static FD_TL uchar scratch_buf[ 4096 ]; + uchar * scratch = scratch_buf; + fd_h2_hdr_t hdr[1]; + uint err = fd_hpack_rd_next( hpack_rd, hdr, &scratch, scratch_buf+sizeof(scratch_buf) ); + if( FD_UNLIKELY( err ) ) { + FD_LOG_WARNING(( "Failed to parse response headers (%u-%s)", err, fd_h2_strerror( err ) )); + return FD_H2_ERR_PROTOCOL; + } + + int hdr_idx = fd_h2_hdr_match( matcher, hdr->name, hdr->name_len, hdr->hint ); + switch( hdr_idx ) { + case FD_H2_HDR_STATUS: + resp->h2_status = fd_grpc_h2_parse_num( hdr->value, hdr->value_len ); + break; + case FD_H2_HDR_CONTENT_TYPE: + resp->is_grpc_proto = + ( 0==strncmp( hdr->value, "application/grpc", hdr->value_len ) || + 0==strncmp( hdr->value, "application/grpc+proto", hdr->value_len ) ); + break; + case FD_GRPC_HDR_STATUS: + resp->grpc_status = fd_grpc_h2_parse_num( hdr->value, hdr->value_len ); + break; + case FD_GRPC_HDR_MESSAGE: + resp->grpc_msg_len = (uint)fd_ulong_min( hdr->value_len, sizeof(resp->grpc_msg) ); + if( resp->grpc_msg_len ) { + fd_memcpy( resp->grpc_msg, hdr->value, resp->grpc_msg_len ); + } + break; + } + } + return FD_H2_SUCCESS; +} + +char const * +fd_grpc_status_cstr( uint status ) { + switch( status ) { + case FD_GRPC_STATUS_OK: return "ok"; + case FD_GRPC_STATUS_CANCELLED: return "cancelled"; + case FD_GRPC_STATUS_UNKNOWN: return "unknown"; + case FD_GRPC_STATUS_INVALID_ARGUMENT: return "invalid argument"; + case FD_GRPC_STATUS_DEADLINE_EXCEEDED: return "deadline exceeded"; + case FD_GRPC_STATUS_NOT_FOUND: return "not found"; + case FD_GRPC_STATUS_ALREADY_EXISTS: return "already exists"; + case FD_GRPC_STATUS_PERMISSION_DENIED: return "permission denied"; + case FD_GRPC_STATUS_RESOURCE_EXHAUSTED: return "resource exhausted"; + case FD_GRPC_STATUS_FAILED_PRECONDITION: return "failed precondition"; + case FD_GRPC_STATUS_ABORTED: return "aborted"; + case FD_GRPC_STATUS_OUT_OF_RANGE: return "out of range"; + case FD_GRPC_STATUS_UNIMPLEMENTED: return "unimplemented"; + case FD_GRPC_STATUS_INTERNAL: return "internal"; + case FD_GRPC_STATUS_UNAVAILABLE: return "unavailable"; + case FD_GRPC_STATUS_DATA_LOSS: return "data loss"; + case FD_GRPC_STATUS_UNAUTHENTICATED: return "unauthenticated"; + default: return "unknown"; + } +} diff --git a/src/waltz/grpc/fd_grpc_codec.h b/src/waltz/grpc/fd_grpc_codec.h new file mode 100644 index 0000000000..99d2e1d0e2 --- /dev/null +++ b/src/waltz/grpc/fd_grpc_codec.h @@ -0,0 +1,103 @@ +#ifndef HEADER_fd_src_waltz_grpc_fd_grpc_codec_h +#define HEADER_fd_src_waltz_grpc_fd_grpc_codec_h + +/* fd_grpc_codec.h provides helpers for gRPC over HTTP/2. + + https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md */ + +#include "../h2/fd_h2_base.h" +#include "../h2/fd_h2_hdr_match.h" + +/* gRPC protocol status codes + https://github.com/grpc/grpc/blob/v1.71.0/doc/statuscodes.md */ + +#define FD_GRPC_STATUS_OK 0 +#define FD_GRPC_STATUS_CANCELLED 1 +#define FD_GRPC_STATUS_UNKNOWN 2 +#define FD_GRPC_STATUS_INVALID_ARGUMENT 3 +#define FD_GRPC_STATUS_DEADLINE_EXCEEDED 4 +#define FD_GRPC_STATUS_NOT_FOUND 5 +#define FD_GRPC_STATUS_ALREADY_EXISTS 6 +#define FD_GRPC_STATUS_PERMISSION_DENIED 7 +#define FD_GRPC_STATUS_RESOURCE_EXHAUSTED 8 +#define FD_GRPC_STATUS_FAILED_PRECONDITION 9 +#define FD_GRPC_STATUS_ABORTED 10 +#define FD_GRPC_STATUS_OUT_OF_RANGE 11 +#define FD_GRPC_STATUS_UNIMPLEMENTED 12 +#define FD_GRPC_STATUS_INTERNAL 13 +#define FD_GRPC_STATUS_UNAVAILABLE 14 +#define FD_GRPC_STATUS_DATA_LOSS 15 +#define FD_GRPC_STATUS_UNAUTHENTICATED 16 + +/* Internal IDs for gRPC headers */ + +#define FD_GRPC_HDR_STATUS 0x101 +#define FD_GRPC_HDR_MESSAGE 0x102 + +/* fd_grpc_hdr_t is the header part of a Length-Prefixed-Message. */ + +struct __attribute__((packed)) fd_grpc_hdr { + uchar compressed; /* in [0,1] */ + uint msg_sz; /* net order */ + /* msg_sz bytes follow ... */ +}; + +typedef struct fd_grpc_hdr fd_grpc_hdr_t; + +struct fd_grpc_req_hdrs { + char const * host; /* excluding port */ + ulong host_len; /* <=255 */ + ushort port; + char const * path; + ulong path_len; + uint https : 1; /* 1 if https, 0 if http */ + char const * bearer_auth; + ulong bearer_auth_len; +}; + +typedef struct fd_grpc_req_hdrs fd_grpc_req_hdrs_t; + +struct fd_grpc_resp_hdrs { + /* Headers */ + + uint h2_status; /* 0 implies invalid */ + uint is_grpc_proto : 1; + + /* Trailers */ + + uint grpc_status; /* 0 implies invalid */ + char grpc_msg[ 1008 ]; + uint grpc_msg_len; +}; + +typedef struct fd_grpc_resp_hdrs fd_grpc_resp_hdrs_t; + +FD_PROTOTYPES_BEGIN + +/* fd_grpc_h2_gen_request_hdrs generates a HEADERS frame with gRPC + request headers. Returns FD_H2_SUCCESS on success. On failure, + returns FD_H2_INTERNAL_ERROR (insufficient space in rbuf_tx). */ + +int +fd_grpc_h2_gen_request_hdrs( fd_grpc_req_hdrs_t const * req, + fd_h2_rbuf_t * rbuf_tx, + char const * version, + ulong version_len ); + +/* fd_grpc_h2_rec_response_hdrs consumes a HEADERS frame and recovers + selected gRPC request headers. Ignores unknown headers. Returns + FD_H2_SUCCESS on success, or FD_H2_ERR_PROTOCOL on parse failure. + Logs reason for failure. */ + +int +fd_grpc_h2_read_response_hdrs( fd_grpc_resp_hdrs_t * resp, + fd_h2_hdr_matcher_t const * matcher, + uchar const * payload, + ulong payload_sz ); + +char const * +fd_grpc_status_cstr( uint status ); + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_waltz_grpc_fd_grpc_codec_h */ diff --git a/src/waltz/grpc/test_grpc_codec.c b/src/waltz/grpc/test_grpc_codec.c new file mode 100644 index 0000000000..ad3ff8bc07 --- /dev/null +++ b/src/waltz/grpc/test_grpc_codec.c @@ -0,0 +1,89 @@ +#include "fd_grpc_codec.h" +#include "../h2/fd_h2_rbuf.h" +#include "../h2/fd_hpack.h" +#include "../../util/fd_util.h" + +static void +test_h2_gen_request_hdr( void ) { + fd_grpc_req_hdrs_t req = { + .https = 1, + .host = "example.org", + .host_len = 11, + .port = 443, + .path = "/auth.AuthService/GenerateAuthChallenge", + .path_len = 39, + }; + uchar buf[ 2048 ]; + fd_h2_rbuf_t rbuf_tx[1]; + fd_h2_rbuf_init( rbuf_tx, buf, sizeof(buf) ); + FD_TEST( fd_grpc_h2_gen_request_hdrs( &req, rbuf_tx, "1.2.3", 5 )==1 ); + FD_TEST( rbuf_tx->lo_off==0 && rbuf_tx->lo==buf ); +# define EXPECT_HDR( nam, val ) \ + do { \ + FD_TEST( !fd_hpack_rd_done( hpack_rd ) ); \ + FD_TEST( !fd_hpack_rd_next( hpack_rd, hdr, &scratch, 0UL ) ); \ + FD_TEST( hdr->name_len==sizeof(nam)-1 ); \ + FD_TEST( fd_memeq( hdr->name, nam, sizeof(nam)-1 ) ); \ + FD_TEST( hdr->value_len==sizeof(val)-1 ); \ + FD_TEST( fd_memeq( hdr->value, val, sizeof(val)-1 ) ); \ + } while(0) + + fd_hpack_rd_t hpack_rd[1]; + fd_hpack_rd_init( hpack_rd, buf, rbuf_tx->hi_off ); + fd_h2_hdr_t hdr[1]; + uchar * scratch = NULL; + EXPECT_HDR( ":method", "POST" ); + EXPECT_HDR( ":scheme", "https" ); + EXPECT_HDR( ":path", "/auth.AuthService/GenerateAuthChallenge" ); + EXPECT_HDR( ":authority", "example.org:443" ); + EXPECT_HDR( "te", "trailers" ); + EXPECT_HDR( "content-type", "application/grpc+proto" ); + EXPECT_HDR( "user-agent", "grpc-firedancer/1.2.3" ); + FD_TEST( fd_hpack_rd_done( hpack_rd ) ); + + char const example_jwt[] = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"; + fd_grpc_req_hdrs_t req2 = { + .https = 1, + .host = "example.org", + .host_len = 11, + .port = 443, + .path = "/block_engine.BlockEngineValidator/SubscribePackets", + .path_len = 51, + .bearer_auth = example_jwt, /* example invalid JWT */ + .bearer_auth_len = sizeof(example_jwt)-1, + }; + fd_h2_rbuf_init( rbuf_tx, buf, sizeof(buf) ); + FD_TEST( fd_grpc_h2_gen_request_hdrs( &req2, rbuf_tx, "1.2.3", 5 )==1 ); + FD_TEST( rbuf_tx->lo_off==0 && rbuf_tx->lo==buf ); + + fd_hpack_rd_init( hpack_rd, buf, rbuf_tx->hi_off ); + EXPECT_HDR( ":method", "POST" ); + EXPECT_HDR( ":scheme", "https" ); + EXPECT_HDR( ":path", "/block_engine.BlockEngineValidator/SubscribePackets" ); + EXPECT_HDR( ":authority", "example.org:443" ); + EXPECT_HDR( "te", "trailers" ); + EXPECT_HDR( "content-type", "application/grpc+proto" ); + EXPECT_HDR( "user-agent", "grpc-firedancer/1.2.3" ); + FD_TEST( !fd_hpack_rd_done( hpack_rd ) ); + FD_TEST( !fd_hpack_rd_next( hpack_rd, hdr, &scratch, 0UL ) ); + FD_TEST( hdr->name_len==13 ); + FD_TEST( fd_memeq( hdr->name, "authorization", 13 ) ); + FD_TEST( hdr->value_len==7+req2.bearer_auth_len ); + FD_TEST( fd_memeq( hdr->value, "Bearer ", 7 ) ); + FD_TEST( fd_memeq( hdr->value+7, req2.bearer_auth, req2.bearer_auth_len ) ); + FD_TEST( fd_hpack_rd_done( hpack_rd ) ); + +# undef EXPECT_HDR +} + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + + test_h2_gen_request_hdr(); + + FD_LOG_NOTICE(( "pass" )); + fd_halt(); + return 0; +} diff --git a/src/waltz/h2/Local.mk b/src/waltz/h2/Local.mk new file mode 100644 index 0000000000..78789baaab --- /dev/null +++ b/src/waltz/h2/Local.mk @@ -0,0 +1,31 @@ +# HPACK +$(call add-hdrs,fd_hpack.h,fd_waltz) +$(call add-objs,fd_hpack,fd_waltz) +$(call add-objs,nghttp2_hd_huffman nghttp2_hd_huffman_data,fd_waltz) +$(call make-fuzz-test,fuzz_hpack_rd,fuzz_hpack_rd,fd_waltz fd_util) + +# HTTP/2 +$(call add-hdrs,fd_h2_base.h fd_h2_proto.h) +$(call add-objs,fd_h2_proto,fd_waltz) + +$(call add-hdrs,fd_h2_rbuf.h fd_h2_rbuf_sock.h) + +$(call add-hdrs,fd_h2_callback.h) +$(call add-objs,fd_h2_callback,fd_waltz) + +$(call add-hdrs,fd_h2_conn.h) +$(call add-objs,fd_h2_conn,fd_waltz) + +$(call add-hdrs,fd_h2_hdr_match.h) +$(call add-objs,fd_h2_hdr_match,fd_waltz) + +$(call add-hdrs,fd_h2_tx.h) +$(call add-objs,fd_h2_tx,fd_waltz) + +# Tests +$(call make-unit-test,test_h2,test_h2,fd_waltz fd_ballet fd_util) +$(call run-unit-test,test_h2) + +$(call make-fuzz-test,fuzz_h2,fuzz_h2,fd_waltz fd_util) + +$(call make-unit-test,test_h2_server,test_h2_server,fd_waltz fd_util) diff --git a/src/waltz/h2/README.md b/src/waltz/h2/README.md new file mode 100644 index 0000000000..510387a62f --- /dev/null +++ b/src/waltz/h2/README.md @@ -0,0 +1,73 @@ +This directory contains an implementation of the HTTP/2 framing layer. + +## Notices + +**HTTP** + +This is not an HTTP library. This library provides a framing layer +only. In other words, RFC 9113 Section 8 is missing entirely. + +**HPACK fragmentation** + +This library assumes that a single header record (HPACK record) is not +fragmented across two HTTP frames (e.g. HEADERS and CONTINUATION). +Throws connection error COMPRESSION_ERROR if the peer does that. + +**Server Push** + +PUSH_PROMISE / HTTP Server Push is not supported (disabled via SETTINGS) + +**Priority** + +HTTP/2 priority hints are ignored. + +**HPACK dynamic table** + +The HPACK dynamic table is not supported (disabled via SETTINGS). + +This may cause compatibility issues when running as a server. The +dynamic table provides stateful HTTP header compression. Unfortunately, +there is a race condition between disabling the dynamic table and the +client's first few requests. A conforming client may generate multiple +requests before seeing our SETTINGS. The second request might reuse a +header from the first request via HPACK, but fd_h2 does not understand +this. + +**END_STREAM / CONTINUATION state** + +> A HEADERS frame with the END_STREAM flag set signals the end of a stream. +> However, a HEADERS frame with the END_STREAM flag set can be followed by +> CONTINUATION frames on the same stream. Logically, the CONTINUATION frames +> are part of the HEADERS frame. + +fd_h2 does not support this correctly. + +## HTTP/2 quirks + +This section points out a few HTTP/2 quirks in general. + +### Header sequence + +In the HTTP/2 framing layer, one may send arbitrarily many field blocks. +Examples of field blocks are headers (mandatory) or trailers (optional). +But a client might also send multiple field blocks before sending data, +giving the appearance that there are conflicting headers. Or even send +field blocks while still transmitting data like an odd form of +out-of-band data. + +### Server requests + +In the HTTP/2 framing layer, the server may initiate streams. This is +unrelated to server push or regular responses. In HTTP semantics, this +is as if the HTTP server sent HTTP requests to the client. + +## Coverage + +```shell +CORPUS=~/corpus/fuzz_h2 # change this +make CC=clang EXTRAS=llvm-cov BUILDDIR=clang-cov -j build/clang-cov/fuzz-test/fuzz_h2 && \ + build/clang-cov/fuzz-test/fuzz_h2 $CORPUS && \ + llvm-profdata merge -o cov.profdata default.profraw && \ + llvm-cov export -format=lcov --instr-profile cov.profdata build/clang-cov/fuzz-test/fuzz_h2 > cov.lcov && \ + genhtml --output report cov.lcov +``` diff --git a/src/waltz/h2/fd_h2.h b/src/waltz/h2/fd_h2.h new file mode 100644 index 0000000000..32c80e13f8 --- /dev/null +++ b/src/waltz/h2/fd_h2.h @@ -0,0 +1,13 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_h2_h +#define HEADER_fd_src_waltz_h2_fd_h2_h + +//#include "fd_h2_base.h" +#include "fd_h2_callback.h" /* includes fd_h2_base.h */ +//#include "fd_h2_proto.h" /* includes fd_h2_base.h */ +//#include "fd_h2_rbuf.h" /* includes fd_h2_base.h */ +//#include "fd_h2_conn.h" /* includes fd_h2_proto.h fd_h2_rbuf.h */ +#include "fd_h2_stream.h" /* includes fd_h2_proto.h fd_h2_conn.h */ +#include "fd_h2_tx.h" /* includes fd_h2_proto.h */ +#include "fd_hpack.h" /* includes fd_h2_base.h */ + +#endif /* HEADER_fd_src_waltz_h2_fd_h2_h */ diff --git a/src/waltz/h2/fd_h2_base.h b/src/waltz/h2/fd_h2_base.h new file mode 100644 index 0000000000..beaa9e4063 --- /dev/null +++ b/src/waltz/h2/fd_h2_base.h @@ -0,0 +1,60 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_h2_base +#define HEADER_fd_src_waltz_h2_fd_h2_base + +#include "../../util/bits/fd_bits.h" + +/* Enable sockets support? */ + +#ifndef FD_H2_HAS_SOCKETS +#if FD_HAS_HOSTED +#define FD_H2_HAS_SOCKETS 1 +#endif +#endif + +#ifndef FD_H2_HAS_SOCKETS +#define FD_H2_HAS_SOCKETS 0 +#endif + +/* Forward declarations for all objects */ + +struct fd_h2_callbacks; +typedef struct fd_h2_callbacks fd_h2_callbacks_t; + +struct fd_h2_rbuf; +typedef struct fd_h2_rbuf fd_h2_rbuf_t; + +struct fd_h2_conn; +typedef struct fd_h2_conn fd_h2_conn_t; + +struct fd_h2_stream; +typedef struct fd_h2_stream fd_h2_stream_t; + +/* HTTP/2 error codes + https://www.iana.org/assignments/http2-parameters/http2-parameters.xhtml#error-code */ + +#define FD_H2_SUCCESS 0x00 +#define FD_H2_ERR_PROTOCOL 0x01 +#define FD_H2_ERR_INTERNAL 0x02 +#define FD_H2_ERR_FLOW_CONTROL 0x03 +#define FD_H2_ERR_SETTINGS_TIMEOUT 0x04 +#define FD_H2_ERR_STREAM_CLOSED 0x05 +#define FD_H2_ERR_FRAME_SIZE 0x06 +#define FD_H2_ERR_REFUSED_STREAM 0x07 +#define FD_H2_ERR_CANCEL 0x08 +#define FD_H2_ERR_COMPRESSION 0x09 +#define FD_H2_ERR_CONNECT 0x0a +#define FD_H2_ERR_ENHANCE_YOUR_CALM 0x0b +#define FD_H2_ERR_INADEQUATE_SECURITY 0x0c +#define FD_H2_ERR_HTTP_1_1_REQUIRED 0x0d + +FD_PROTOTYPES_BEGIN + +/* fd_h2_strerror returns a static-lifetime cstr briefly describing the + given FD_H2_ERR_* code. */ + +FD_FN_CONST char const * +fd_h2_strerror( uint err ); + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_waltz_h2_fd_h2_base */ diff --git a/src/waltz/h2/fd_h2_callback.c b/src/waltz/h2/fd_h2_callback.c new file mode 100644 index 0000000000..a8bd3652cf --- /dev/null +++ b/src/waltz/h2/fd_h2_callback.c @@ -0,0 +1,91 @@ +#include "fd_h2_callback.h" +#include "fd_h2_base.h" + +fd_h2_stream_t * +fd_h2_noop_stream_create( fd_h2_conn_t * conn, + uint stream_id ) { + (void)conn; (void)stream_id; + return NULL; +} + +fd_h2_stream_t * +fd_h2_noop_stream_query( fd_h2_conn_t * conn, + uint stream_id ) { + (void)conn; (void)stream_id; + return NULL; +} + +void +fd_h2_noop_conn_established( fd_h2_conn_t * conn ) { + (void)conn; +} + +void +fd_h2_noop_conn_final( fd_h2_conn_t * conn, + uint h2_err, + int closed_by ) { + (void)conn; (void)h2_err; (void)closed_by; +} + +void +fd_h2_noop_headers( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + void const * data, + ulong data_sz, + ulong flags ) { + (void)conn; (void)stream; (void)data; (void)data_sz; (void)flags; +} + +void +fd_h2_noop_data( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + void const * data, + ulong data_sz, + ulong flags ) { + (void)conn; (void)stream; (void)data; (void)data_sz; (void)flags; +} + +void +fd_h2_noop_rst_stream( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + uint error_code, + int closed_by ) { + (void)conn; (void)stream; (void)error_code; (void)closed_by; +} + +void +fd_h2_noop_window_update( fd_h2_conn_t * conn, + uint increment ) { + (void)conn; (void)increment; +} + +void +fd_h2_noop_stream_window_update( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + uint increment ) { + (void)conn; (void)stream; (void)increment; +} + +void +fd_h2_noop_ping_ack( fd_h2_conn_t * conn ) { + (void)conn; +} + +fd_h2_callbacks_t const fd_h2_callbacks_noop = { + .stream_create = fd_h2_noop_stream_create, + .stream_query = fd_h2_noop_stream_query, + .conn_established = fd_h2_noop_conn_established, + .conn_final = fd_h2_noop_conn_final, + .headers = fd_h2_noop_headers, + .data = fd_h2_noop_data, + .rst_stream = fd_h2_noop_rst_stream, + .window_update = fd_h2_noop_window_update, + .stream_window_update = fd_h2_noop_stream_window_update, + .ping_ack = fd_h2_noop_ping_ack, +}; + +fd_h2_callbacks_t * +fd_h2_callbacks_init( fd_h2_callbacks_t * callbacks ) { + *callbacks = fd_h2_callbacks_noop; + return callbacks; +} diff --git a/src/waltz/h2/fd_h2_callback.h b/src/waltz/h2/fd_h2_callback.h new file mode 100644 index 0000000000..39d436d101 --- /dev/null +++ b/src/waltz/h2/fd_h2_callback.h @@ -0,0 +1,158 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_h2_callback_h +#define HEADER_fd_src_waltz_h2_fd_h2_callback_h + +/* fd_h2_callback.h defines the callback interface that fd_h2 uses to + notify apps of events on existing connections. */ + +#include "fd_h2_base.h" + +/* fd_h2_callbacks_t is a virtual function table. May not contain NULL + pointers. */ + +struct fd_h2_callbacks { + + /* stream_create requests the callee to allocate a stream object for + a peer-initiated HTTP/2 stream. + + The returned pointer hould be valid until at least when fd_h2_rx + returns, which is the function that issues the callback. The + callee initializes the stream with fd_h2_stream_init. The user of + this API promises to eventually close the stream with + fd_h2_stream_close (unless the fd_h2_conn is destroyed). + + Returns NULL if the app rejects the creation of the stream. */ + + fd_h2_stream_t * + (* stream_create)( fd_h2_conn_t * conn, + uint stream_id ); + + /* stream_query queries for a previously created stream. */ + + fd_h2_stream_t * + (* stream_query)( fd_h2_conn_t * conn, + uint stream_id ); + + void + (* conn_established)( fd_h2_conn_t * conn ); + + /* conn_final notifies the caller just before conn closes. */ + + void + (* conn_final)( fd_h2_conn_t * conn, + uint h2_err, + int closed_by ); /* 0=local 1=peer */ + + /* headers delivers a chunk of incoming HPACK-encoded header data. + the low bits of flags are the frame flags (e.g. END_STREAM or + END_HEADERS). If FD_H2_VFLAG_CONTINUATION is set, indicates that + the header block comes from a continuation frame. */ + + void + (* headers)( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + void const * data, + ulong data_sz, + ulong flags ); + + /* data delivers a chunk of incoming raw stream data. Not necessarily + aligned to an HTTP/2 frame. */ + + void + (* data)( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + void const * data, + ulong data_sz, + ulong flags ); + + /* rst_stream signals termination of a stream. The callee should + deallocate the stream object and cease writing on it. */ + + void + (* rst_stream)( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + uint error_code, + int closed_by ); /* 0=local 1=peer */ + + /* window_update delivers a conn-level WINDOW_UPDATE frame. */ + + void + (* window_update)( fd_h2_conn_t * conn, + uint increment ); + + /* stream_window_update delivers a stream-level WINDOW_UPDATE frame. */ + + void + (* stream_window_update)( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + uint increment ); + + /* ping_ack delivers an acknowledgement of a PING that was previously + sent by fd_h2_tx_ping. */ + + void + (* ping_ack)( fd_h2_conn_t * conn ); + +}; + +FD_PROTOTYPES_BEGIN + +extern fd_h2_callbacks_t const fd_h2_callbacks_noop; + +/* fd_h2_callbacks_init initializes callbacks no-op functions. */ + +fd_h2_callbacks_t * +fd_h2_callbacks_init( fd_h2_callbacks_t * callbacks ); + +/* Stubs / default functions */ + +fd_h2_stream_t * +fd_h2_noop_stream_create( fd_h2_conn_t * conn, + uint stream_id ); + +fd_h2_stream_t * +fd_h2_noop_stream_query( fd_h2_conn_t * conn, + uint stream_id ); + +void +fd_h2_noop_conn_established( fd_h2_conn_t * conn ); + +void +fd_h2_noop_conn_final( fd_h2_conn_t * conn, + uint h2_err, + int closed_by ); + +void +fd_h2_noop_headers( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + void const * data, + ulong data_sz, + ulong flags ); + +void +fd_h2_noop_data( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + void const * data, + ulong data_sz, + ulong flags ); + +void +fd_h2_noop_rst_stream( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + uint error_code, + int closed_by ); + +void +fd_h2_noop_window_update( fd_h2_conn_t * conn, + uint increment ); + +void +fd_h2_noop_stream_window_update( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + uint increment ); + +void +fd_h2_noop_ping_ack( fd_h2_conn_t * conn ); + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_waltz_h2_fd_h2_callback_h */ diff --git a/src/waltz/h2/fd_h2_conn.c b/src/waltz/h2/fd_h2_conn.c new file mode 100644 index 0000000000..85001994b6 --- /dev/null +++ b/src/waltz/h2/fd_h2_conn.c @@ -0,0 +1,794 @@ +#include "fd_h2_conn.h" +#include "fd_h2_callback.h" +#include "fd_h2_proto.h" +#include "fd_h2_rbuf.h" +#include "fd_h2_stream.h" +#include + +char const fd_h2_client_preface[24] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"; + +static fd_h2_settings_t const fd_h2_settings_initial = { + .max_concurrent_streams = UINT_MAX, + .initial_window_size = 65535U, + .max_frame_size = 16384U, + .max_header_list_size = UINT_MAX +}; + +static void +fd_h2_conn_init_window( fd_h2_conn_t * conn ) { + conn->rx_wnd_max = 65535U; + conn->rx_wnd = conn->rx_wnd_max; + conn->rx_wnd_wmark = (uint)( 0.7f * (float)conn->rx_wnd_max ); + conn->tx_wnd = 65535U; +} + +fd_h2_conn_t * +fd_h2_conn_init_client( fd_h2_conn_t * conn ) { + *conn = (fd_h2_conn_t) { + .self_settings = fd_h2_settings_initial, + .peer_settings = fd_h2_settings_initial, + .flags = FD_H2_CONN_FLAGS_CLIENT_INITIAL, + .tx_stream_next = 1U, + .rx_stream_next = 2U + }; + fd_h2_conn_init_window( conn ); + return conn; +} + +fd_h2_conn_t * +fd_h2_conn_init_server( fd_h2_conn_t * conn ) { + *conn = (fd_h2_conn_t) { + .self_settings = fd_h2_settings_initial, + .peer_settings = fd_h2_settings_initial, + .flags = FD_H2_CONN_FLAGS_SERVER_INITIAL, + .tx_stream_next = 2U, + .rx_stream_next = 1U + }; + fd_h2_conn_init_window( conn ); + return conn; +} + +static void +fd_h2_setting_encode( uchar * buf, + ushort setting_id, + uint setting_value ) { + FD_STORE( ushort, buf, fd_ushort_bswap( setting_id ) ); + FD_STORE( uint, buf+2, fd_uint_bswap ( setting_value ) ); +} + +#define FD_H2_OUR_SETTINGS_ENCODED_SZ 45 + +static void +fd_h2_gen_settings( fd_h2_settings_t const * settings, + uchar buf[ FD_H2_OUR_SETTINGS_ENCODED_SZ ] ) { + fd_h2_frame_hdr_t hdr = { + .typlen = fd_h2_frame_typlen( FD_H2_FRAME_TYPE_SETTINGS, 36UL ), + }; + fd_memcpy( buf, &hdr, 9UL ); + + fd_h2_setting_encode( buf+9, FD_H2_SETTINGS_HEADER_TABLE_SIZE, 0U ); + fd_h2_setting_encode( buf+15, FD_H2_SETTINGS_ENABLE_PUSH, 0U ); + fd_h2_setting_encode( buf+21, FD_H2_SETTINGS_MAX_CONCURRENT_STREAMS, settings->max_concurrent_streams ); + fd_h2_setting_encode( buf+27, FD_H2_SETTINGS_INITIAL_WINDOW_SIZE, settings->initial_window_size ); + fd_h2_setting_encode( buf+33, FD_H2_SETTINGS_MAX_FRAME_SIZE, settings->max_frame_size ); + fd_h2_setting_encode( buf+39, FD_H2_SETTINGS_MAX_HEADER_LIST_SIZE, settings->max_header_list_size ); +} + +/* fd_h2_rx_data handles a partial DATA frame. */ + +static void +fd_h2_rx_data( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_rx, + fd_h2_rbuf_t * rbuf_tx, + fd_h2_callbacks_t const * cb ) { + /* A receive might generate two WINDOW_UPDATE frames */ + if( FD_UNLIKELY( fd_h2_rbuf_free_sz( rbuf_tx ) < 2*sizeof(fd_h2_window_update_t) ) ) return; + + ulong frame_rem = conn->rx_frame_rem; + ulong rbuf_avail = fd_h2_rbuf_used_sz( rbuf_rx ); + uint stream_id = conn->rx_stream_id; + uint chunk_sz = (uint)fd_ulong_min( frame_rem, rbuf_avail ); + uint fin_flag = conn->rx_frame_flags & FD_H2_FLAG_END_STREAM; + if( rbuf_availstream_query( conn, stream_id ); + if( FD_UNLIKELY( !stream || + ( stream->state!=FD_H2_STREAM_STATE_OPEN && + stream->state!=FD_H2_STREAM_STATE_CLOSING_TX ) ) ) { + fd_h2_stream_error1( rbuf_tx, stream_id, FD_H2_ERR_STREAM_CLOSED ); + goto skip_frame; + } + + if( FD_UNLIKELY( chunk_sz > conn->rx_wnd ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_FLOW_CONTROL ); + return; + } + conn->rx_wnd -= chunk_sz; + + if( FD_UNLIKELY( chunk_sz > stream->rx_wnd ) ) { + fd_h2_stream_error1( rbuf_tx, stream_id, FD_H2_ERR_FLOW_CONTROL ); + goto skip_frame; + } + stream->rx_wnd -= chunk_sz; + + fd_h2_stream_rx_data( stream, conn, fin_flag ? FD_H2_FLAG_END_STREAM : 0U ); + if( FD_UNLIKELY( stream->state==FD_H2_STREAM_STATE_ILLEGAL ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return; + } + + ulong sz0, sz1; + uchar const * peek = fd_h2_rbuf_peek_used( rbuf_rx, &sz0, &sz1 ); + if( sz0>=chunk_sz ) { + sz0 = chunk_sz; + sz1 = 0; + } else if( sz0+sz1>chunk_sz ) { + sz1 = chunk_sz-sz0; + } + if( FD_LIKELY( !sz1 ) ) { + cb->data( conn, stream, peek, sz0, fin_flag ); + } else { + cb->data( conn, stream, peek, sz0, 0 ); + cb->data( conn, stream, rbuf_rx->buf0, sz1, fin_flag ); + } + +skip_frame: + conn->rx_frame_rem -= chunk_sz; + fd_h2_rbuf_skip( rbuf_rx, chunk_sz ); + if( FD_UNLIKELY( conn->rx_wnd < conn->rx_wnd_wmark ) ) { + conn->flags |= FD_H2_CONN_FLAGS_WINDOW_UPDATE; + } +} + +static int +fd_h2_rx_headers( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_tx, + uchar * payload, + ulong payload_sz, + fd_h2_callbacks_t const * cb, + uint frame_flags, + uint stream_id ) { + + if( FD_UNLIKELY( !stream_id ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + + fd_h2_stream_t * stream = cb->stream_query( conn, stream_id ); + if( !stream ) { + if( FD_UNLIKELY( ( stream_id < conn->rx_stream_next ) | + ( (stream_id&1) != (conn->rx_stream_next&1) ) ) ) { + /* FIXME should send RST_STREAM instead if the user deallocated + stream state but we receive a HEADERS frame for a stream that + we started ourselves. */ + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + if( FD_UNLIKELY( conn->stream_active_cnt[0] >= conn->self_settings.max_concurrent_streams ) ) { + fd_h2_stream_error1( rbuf_tx, stream_id, FD_H2_ERR_REFUSED_STREAM ); + return 1; + } + stream = cb->stream_create( conn, stream_id ); + if( FD_UNLIKELY( !stream ) ) { + fd_h2_stream_error1( rbuf_tx, stream_id, FD_H2_ERR_REFUSED_STREAM ); + return 1; + } + fd_h2_stream_open( stream, conn, stream_id ); + stream->tx_wnd = conn->peer_settings.initial_window_size; + conn->stream_active_cnt[0]++; + conn->rx_stream_next = stream_id+2; + } + + conn->rx_stream_id = stream_id; + + if( FD_UNLIKELY( frame_flags & FD_H2_FLAG_PRIORITY ) ) { + if( FD_UNLIKELY( payload_sz<5UL ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_FRAME_SIZE ); + return 0; + } + payload += 5UL; + payload_sz -= 5UL; + } + + if( FD_UNLIKELY( !( frame_flags & FD_H2_FLAG_END_HEADERS ) ) ) { + conn->flags |= FD_H2_CONN_FLAGS_CONTINUATION; + } + + fd_h2_stream_rx_headers( stream, conn, frame_flags ); + if( FD_UNLIKELY( stream->state==FD_H2_STREAM_STATE_ILLEGAL ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + + cb->headers( conn, stream, payload, payload_sz, frame_flags ); + + return 1; +} + +static int +fd_h2_rx_priority( fd_h2_conn_t * conn, + ulong payload_sz, + uint stream_id ) { + if( FD_UNLIKELY( payload_sz!=5UL ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_FRAME_SIZE ); + return 0; + } + if( FD_UNLIKELY( !stream_id ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + return 1; +} + +static int +fd_h2_rx_continuation( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_tx, + uchar * payload, + ulong payload_sz, + fd_h2_callbacks_t const * cb, + uint frame_flags, + uint stream_id ) { + + if( FD_UNLIKELY( ( conn->rx_stream_id!=stream_id ) | + ( !( conn->flags & FD_H2_CONN_FLAGS_CONTINUATION ) ) | + ( !stream_id ) ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + + if( FD_UNLIKELY( frame_flags & FD_H2_FLAG_END_HEADERS ) ) { + conn->flags &= (uchar)~FD_H2_CONN_FLAGS_CONTINUATION; + } + + fd_h2_stream_t * stream = cb->stream_query( conn, stream_id ); + if( FD_UNLIKELY( !stream ) ) { + fd_h2_stream_error1( rbuf_tx, stream_id, FD_H2_ERR_INTERNAL ); + return 1; + } + + fd_h2_stream_rx_headers( stream, conn, frame_flags ); + if( FD_UNLIKELY( stream->state==FD_H2_STREAM_STATE_ILLEGAL ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + + cb->headers( conn, stream, payload, payload_sz, frame_flags ); + + return 1; +} + +static int +fd_h2_rx_rst_stream( fd_h2_conn_t * conn, + uchar const * payload, + ulong payload_sz, + fd_h2_callbacks_t const * cb, + uint stream_id ) { + if( FD_UNLIKELY( payload_sz!=4UL ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_FRAME_SIZE ); + return 0; + } + if( FD_UNLIKELY( !stream_id ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + if( FD_UNLIKELY( stream_id >= fd_ulong_max( conn->rx_stream_next, conn->tx_stream_next ) ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + fd_h2_stream_t * stream = cb->stream_query( conn, stream_id ); + if( FD_LIKELY( stream ) ) { + uint error_code = fd_uint_bswap( FD_LOAD( uint, payload ) ); + fd_h2_stream_reset( stream, conn ); + cb->rst_stream( conn, stream, error_code, 1 ); + /* stream points to freed memory at this point */ + } + return 1; +} + +static int +fd_h2_rx_settings( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_tx, + uchar const * payload, + ulong payload_sz, + fd_h2_callbacks_t const * cb, + uint frame_flags, + uint stream_id ) { + + if( FD_UNLIKELY( stream_id ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + + if( FD_UNLIKELY( conn->flags & FD_H2_CONN_FLAGS_SERVER_INITIAL ) ) { + /* As a server, the first frame we should send is SETTINGS, not + SETTINGS ACK as generated here */ + return 0; + } + + if( frame_flags & FD_H2_FLAG_ACK ) { + if( FD_UNLIKELY( payload_sz ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_FRAME_SIZE ); + return 0; + } + if( FD_UNLIKELY( !conn->setting_tx ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + if( conn->flags & FD_H2_CONN_FLAGS_WAIT_SETTINGS_ACK_0 ) { + conn->flags &= (uchar)~FD_H2_CONN_FLAGS_WAIT_SETTINGS_ACK_0; + if( !( conn->flags & FD_H2_CONN_FLAGS_HANDSHAKING ) ) { + cb->conn_established( conn ); + } + } + conn->setting_tx--; + return 1; + } + + if( FD_UNLIKELY( payload_sz % 6 ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_FRAME_SIZE ); + return 0; + } + + for( ulong off=0UL; off1 ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + break; + case FD_H2_SETTINGS_INITIAL_WINDOW_SIZE: + if( FD_UNLIKELY( value>0x7fffffff ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_FLOW_CONTROL ); + return 0; + } + conn->peer_settings.initial_window_size = value; + /* FIXME update window accordingly */ + break; + case FD_H2_SETTINGS_MAX_FRAME_SIZE: + if( FD_UNLIKELY( value<0x4000 || value>0xffffff ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_FLOW_CONTROL ); + return 0; + } + conn->peer_settings.max_frame_size = value; + /* FIXME validate min */ + break; + case FD_H2_SETTINGS_MAX_HEADER_LIST_SIZE: + conn->peer_settings.max_header_list_size = value; + break; + case FD_H2_SETTINGS_MAX_CONCURRENT_STREAMS: + conn->peer_settings.max_concurrent_streams = value; + break; + } + } + + fd_h2_frame_hdr_t hdr = { + .typlen = fd_h2_frame_typlen( FD_H2_FRAME_TYPE_SETTINGS, 0UL ), + .flags = FD_H2_FLAG_ACK + }; + if( FD_UNLIKELY( fd_h2_rbuf_free_sz( rbuf_tx )flags & FD_H2_CONN_FLAGS_WAIT_SETTINGS_0 ) { + conn->flags &= (uchar)~FD_H2_CONN_FLAGS_WAIT_SETTINGS_0; + if( !( conn->flags & FD_H2_CONN_FLAGS_HANDSHAKING ) ) { + cb->conn_established( conn ); + } + } + + return 1; +} + +static int +fd_h2_rx_push_promise( fd_h2_conn_t * conn ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; +} + +static int +fd_h2_rx_ping( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_tx, + uchar const * payload, + ulong payload_sz, + fd_h2_callbacks_t const * cb, + uint frame_flags, + uint stream_id ) { + if( FD_UNLIKELY( payload_sz!=8UL ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_FRAME_SIZE ); + return 0; + } + if( FD_UNLIKELY( stream_id ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + + if( FD_UNLIKELY( frame_flags & FD_H2_FLAG_ACK ) ) { + + /* Received an acknowledgement for a PING frame. */ + if( FD_UNLIKELY( conn->ping_tx==0 ) ) { + /* Unsolicited PING ACK ... Blindly ignore, since RFC 9113 + technically doesn't forbid those. */ + return 1; + } + cb->ping_ack( conn ); + conn->ping_tx = (uchar)( conn->ping_tx-1 ); + + } else { + + /* Received a new PING frame. Generate a PONG. */ + /* FIMXE rate limit */ + fd_h2_ping_t pong = { + .hdr = { + .typlen = fd_h2_frame_typlen( FD_H2_FRAME_TYPE_PING, 8UL ), + .flags = FD_H2_FLAG_ACK, + }, + .payload = FD_LOAD( ulong, payload ) + }; + if( FD_UNLIKELY( fd_h2_rbuf_free_sz( rbuf_tx )ping_tx; + if( FD_UNLIKELY( ( fd_h2_rbuf_free_sz( rbuf_tx )=UCHAR_MAX ) ) ) { + return 0; /* blocked */ + } + + fd_h2_ping_t ping = { + .hdr = { + .typlen = fd_h2_frame_typlen( FD_H2_FRAME_TYPE_PING, 8UL ), + .flags = 0U, + .r_stream_id = 0UL + }, + .payload = 0UL + }; + fd_h2_rbuf_push( rbuf_tx, &ping, sizeof(fd_h2_ping_t) ); + conn->ping_tx = (uchar)( ping_tx+1 ); + return 1; +} + +static int +fd_h2_rx_goaway( fd_h2_conn_t * conn, + fd_h2_callbacks_t const * cb, + uchar const * payload, + ulong payload_sz, + uint stream_id ) { + + if( FD_UNLIKELY( stream_id ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + if( FD_UNLIKELY( payload_sz<8UL ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_FRAME_SIZE ); + return 0; + } + + uint error_code = fd_uint_bswap( FD_LOAD( uint, payload+4UL ) ); + conn->flags = FD_H2_CONN_FLAGS_DEAD; + cb->conn_final( conn, error_code, 1 /* peer */ ); + + return 1; +} + +static int +fd_h2_rx_window_update( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_tx, + fd_h2_callbacks_t const * cb, + uchar const * payload, + ulong payload_sz, + uint stream_id ) { + if( FD_UNLIKELY( payload_sz!=4UL ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_FRAME_SIZE ); + return 0; + } + uint increment = fd_uint_bswap( FD_LOAD( uint, payload ) ) & 0x7fffffff; + + if( !stream_id ) { + + /* Connection-level window update */ + uint tx_wnd = conn->tx_wnd; + if( FD_UNLIKELY( !increment ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + uint tx_wnd_new; + if( FD_UNLIKELY( __builtin_uadd_overflow( tx_wnd, increment, &tx_wnd_new ) ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_FLOW_CONTROL ); + return 0; + } + conn->tx_wnd = tx_wnd_new; + cb->window_update( conn, (uint)increment ); + + } else { + + if( FD_UNLIKELY( stream_id >= fd_ulong_max( conn->rx_stream_next, conn->tx_stream_next ) ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return 0; + } + + if( FD_UNLIKELY( !increment ) ) { + fd_h2_stream_error1( rbuf_tx, stream_id, FD_H2_ERR_PROTOCOL ); + return 1; + } + + fd_h2_stream_t * stream = cb->stream_query( conn, stream_id ); + if( FD_UNLIKELY( !stream ) ) { + fd_h2_stream_error1( rbuf_tx, stream_id, FD_H2_ERR_STREAM_CLOSED ); + return 1; + } + + /* Stream-level window update */ + uint tx_wnd_new; + if( FD_UNLIKELY( __builtin_uadd_overflow( stream->tx_wnd, increment, &tx_wnd_new ) ) ) { + fd_h2_stream_error( stream, rbuf_tx, FD_H2_ERR_FLOW_CONTROL ); + cb->rst_stream( conn, stream, FD_H2_ERR_FLOW_CONTROL, 0 ); + /* stream points to freed memory at this point */ + return 1; + } + stream->tx_wnd = tx_wnd_new; + cb->stream_window_update( conn, stream, (uint)increment ); + + } + + return 1; +} + +/* fd_h2_rx_frame handles a complete frame. Returns 1 on success, and + 0 on connection error. */ + +static int +fd_h2_rx_frame( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_tx, + uchar * payload, + ulong payload_sz, + fd_h2_callbacks_t const * cb, + uint frame_type, + uint frame_flags, + uint stream_id ) { + switch( frame_type ) { + case FD_H2_FRAME_TYPE_HEADERS: + return fd_h2_rx_headers( conn, rbuf_tx, payload, payload_sz, cb, frame_flags, stream_id ); + case FD_H2_FRAME_TYPE_PRIORITY: + return fd_h2_rx_priority( conn, payload_sz, stream_id ); + case FD_H2_FRAME_TYPE_RST_STREAM: + return fd_h2_rx_rst_stream( conn, payload, payload_sz, cb, stream_id ); + case FD_H2_FRAME_TYPE_SETTINGS: + return fd_h2_rx_settings( conn, rbuf_tx, payload, payload_sz, cb, frame_flags, stream_id ); + case FD_H2_FRAME_TYPE_PUSH_PROMISE: + return fd_h2_rx_push_promise( conn ); + case FD_H2_FRAME_TYPE_CONTINUATION: + return fd_h2_rx_continuation( conn, rbuf_tx, payload, payload_sz, cb, frame_flags, stream_id ); + case FD_H2_FRAME_TYPE_PING: + return fd_h2_rx_ping( conn, rbuf_tx, payload, payload_sz, cb, frame_flags, stream_id ); + case FD_H2_FRAME_TYPE_GOAWAY: + return fd_h2_rx_goaway( conn, cb, payload, payload_sz, stream_id ); + case FD_H2_FRAME_TYPE_WINDOW_UPDATE: + return fd_h2_rx_window_update( conn, rbuf_tx, cb, payload, payload_sz, stream_id ); + default: + return 1; + } +} + +/* fd_h2_rx1 handles one frame. */ + +static void +fd_h2_rx1( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_rx, + fd_h2_rbuf_t * rbuf_tx, + uchar * scratch, + ulong scratch_sz, + fd_h2_callbacks_t const * cb ) { + /* All frames except DATA are fully buffered, thus assume that current + frame is a DATA frame if rx_frame_rem != 0. */ + if( conn->rx_frame_rem ) { + fd_h2_rx_data( conn, rbuf_rx, rbuf_tx, cb ); + return; + } + if( FD_UNLIKELY( conn->rx_pad_rem ) ) { + ulong pad_rem = conn->rx_pad_rem; + ulong rbuf_avail = fd_h2_rbuf_used_sz( rbuf_rx ); + uint chunk_sz = (uint)fd_ulong_min( pad_rem, rbuf_avail ); + fd_h2_rbuf_skip( rbuf_rx, chunk_sz ); + conn->rx_pad_rem = (uchar)( conn->rx_pad_rem - chunk_sz ); + return; + } + + /* A new frame starts. Peek the header. */ + if( FD_UNLIKELY( fd_h2_rbuf_used_sz( rbuf_rx )rx_suppress = rbuf_rx->lo_off + sizeof(fd_h2_frame_hdr_t); + return; + } + fd_h2_rbuf_t rx_peek = *rbuf_rx; + fd_h2_frame_hdr_t hdr; + fd_h2_rbuf_pop_copy( &rx_peek, &hdr, sizeof(fd_h2_frame_hdr_t) ); + uint const frame_type = fd_h2_frame_type ( hdr.typlen ); + uint const frame_sz = fd_h2_frame_length( hdr.typlen ); + + if( FD_UNLIKELY( frame_sz > conn->self_settings.max_frame_size ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_FRAME_SIZE ); + return; + } + if( FD_UNLIKELY( (!!( conn->flags & FD_H2_CONN_FLAGS_CONTINUATION ) ) & + ( frame_type!=FD_H2_FRAME_TYPE_CONTINUATION ) ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return; + } + + /* Peek padding */ + uint pad_sz = 0U; + uint rem_sz = frame_sz; + if( ( frame_type==FD_H2_FRAME_TYPE_DATA || + frame_type==FD_H2_FRAME_TYPE_HEADERS || + frame_type==FD_H2_FRAME_TYPE_PUSH_PROMISE ) && + !!( hdr.flags & FD_H2_FLAG_PADDED ) ) { + if( FD_UNLIKELY( fd_h2_rbuf_used_sz( &rx_peek )<1UL ) ) return; + pad_sz = rx_peek.lo[0]; + rem_sz--; + if( FD_UNLIKELY( pad_sz>=rem_sz ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return; + } + fd_h2_rbuf_skip( &rx_peek, 1UL ); + } + + /* Special case: Process data incrementally */ + if( frame_type==FD_H2_FRAME_TYPE_DATA ) { + conn->rx_frame_rem = rem_sz; + conn->rx_frame_flags = hdr.flags; + conn->rx_stream_id = fd_h2_frame_stream_id( hdr.r_stream_id ); + conn->rx_pad_rem = (uchar)pad_sz; + *rbuf_rx = rx_peek; + if( FD_UNLIKELY( !conn->rx_stream_id ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return; + } + fd_h2_rx_data( conn, rbuf_rx, rbuf_tx, cb ); + return; + } + + /* Consume all or nothing */ + ulong const tot_sz = sizeof(fd_h2_frame_hdr_t) + frame_sz; + if( FD_UNLIKELY( tot_sz>fd_h2_rbuf_used_sz( rbuf_rx ) ) ) { + conn->rx_suppress = rbuf_rx->lo_off + tot_sz; + return; + } + + uint payload_sz = rem_sz-pad_sz; + if( FD_UNLIKELY( scratch_sz < payload_sz ) ) { + if( FD_UNLIKELY( scratch_sz < conn->self_settings.max_frame_size ) ) { + FD_LOG_WARNING(( "scratch buffer too small: scratch_sz=%lu max_frame_size=%u)", + scratch_sz, conn->self_settings.max_frame_size )); + fd_h2_conn_error( conn, FD_H2_ERR_INTERNAL ); + return; + } + fd_h2_conn_error( conn, FD_H2_ERR_FRAME_SIZE ); + return; + } + + *rbuf_rx = rx_peek; + uchar * frame = fd_h2_rbuf_pop( rbuf_rx, scratch, payload_sz ); + int ok = + fd_h2_rx_frame( conn, rbuf_tx, frame, payload_sz, cb, + frame_type, + hdr.flags, + fd_h2_frame_stream_id( hdr.r_stream_id ) ); + (void)ok; /* FIXME */ + fd_h2_rbuf_skip( rbuf_rx, pad_sz ); +} + +void +fd_h2_rx( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_rx, + fd_h2_rbuf_t * rbuf_tx, + uchar * scratch, + ulong scratch_sz, + fd_h2_callbacks_t const * cb ) { + /* Pre-receive TX work */ + + /* Stop handling frames on conn error. */ + if( FD_UNLIKELY( conn->flags & FD_H2_CONN_FLAGS_DEAD ) ) return; + + /* All other logic below can only proceed if new data arrived. */ + if( FD_UNLIKELY( !fd_h2_rbuf_used_sz( rbuf_rx ) ) ) return; + + /* Slowloris defense: Guess how much bytes are required to progress + ahead of time based on the frame's type and size. */ + if( FD_UNLIKELY( rbuf_rx->hi_off < conn->rx_suppress ) ) return; + + /* Handle frames */ + for(;;) { + ulong lo0 = rbuf_rx->lo_off; + fd_h2_rx1( conn, rbuf_rx, rbuf_tx, scratch, scratch_sz, cb ); + ulong lo1 = rbuf_rx->lo_off; + + /* Terminate when no more bytes are available to read */ + if( !fd_h2_rbuf_used_sz( rbuf_rx ) ) break; + + /* Terminate when the frame handler didn't make progress (e.g. due + to rbuf_tx full, or due to incomplete read from rbuf_tx)*/ + if( FD_UNLIKELY( lo0==lo1 ) ) break; + + /* Terminate if the conn died */ + if( FD_UNLIKELY( conn->flags & (FD_H2_CONN_FLAGS_SEND_GOAWAY|FD_H2_CONN_FLAGS_DEAD) ) ) break; + } +} + +void +fd_h2_tx_control( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_tx, + fd_h2_callbacks_t const * cb ) { + + if( FD_UNLIKELY( fd_h2_rbuf_free_sz( rbuf_tx )<128 ) ) return; + + switch( fd_uint_find_lsb( (uint)conn->flags | 0x10000u ) ) { + + case FD_H2_CONN_FLAGS_LG_CLIENT_INITIAL: + fd_h2_rbuf_push( rbuf_tx, fd_h2_client_preface, sizeof(fd_h2_client_preface) ); + __attribute__((fallthrough)); + + case FD_H2_CONN_FLAGS_LG_SERVER_INITIAL: { + uchar buf[ FD_H2_OUR_SETTINGS_ENCODED_SZ ]; + fd_h2_gen_settings( &conn->self_settings, buf ); + fd_h2_rbuf_push( rbuf_tx, buf, sizeof(buf) ); + conn->setting_tx++; + conn->flags = FD_H2_CONN_FLAGS_WAIT_SETTINGS_0 | FD_H2_CONN_FLAGS_WAIT_SETTINGS_ACK_0; + break; + } + +goaway: + case FD_H2_CONN_FLAGS_LG_SEND_GOAWAY: { + fd_h2_goaway_t goaway = { + .hdr = { + .typlen = fd_h2_frame_typlen( FD_H2_FRAME_TYPE_GOAWAY, 8UL ) + }, + .last_stream_id = 0, /* FIXME */ + .error_code = fd_uint_bswap( (uint)conn->conn_error ) + }; + conn->flags = FD_H2_CONN_FLAGS_DEAD; + fd_h2_rbuf_push( rbuf_tx, &goaway, sizeof(fd_h2_goaway_t) ); + cb->conn_final( conn, conn->conn_error, 0 /* local */ ); + break; + } + + case FD_H2_CONN_FLAGS_LG_WINDOW_UPDATE: { + uint increment = conn->rx_wnd_max - conn->rx_wnd; + if( FD_UNLIKELY( increment>0x7fffffff ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_INTERNAL ); + goto goaway; + } + if( FD_UNLIKELY( increment==0 ) ) break; + fd_h2_window_update_t window_update = { + .hdr = { + .typlen = fd_h2_frame_typlen( FD_H2_FRAME_TYPE_WINDOW_UPDATE, 4UL ) + }, + .increment = fd_uint_bswap( increment ) + }; + fd_h2_rbuf_push( rbuf_tx, &window_update, sizeof(fd_h2_window_update_t) ); + conn->rx_wnd = conn->rx_wnd_max; + conn->flags = (ushort)( (conn->flags) & (~FD_H2_CONN_FLAGS_WINDOW_UPDATE) ); + break; + } + + default: + break; + + } + +} diff --git a/src/waltz/h2/fd_h2_conn.h b/src/waltz/h2/fd_h2_conn.h new file mode 100644 index 0000000000..4e1ccc4051 --- /dev/null +++ b/src/waltz/h2/fd_h2_conn.h @@ -0,0 +1,257 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_h2_conn_h +#define HEADER_fd_src_waltz_h2_fd_h2_conn_h + +/* fd_h2_conn.h provides the HTTP/2 connection state machine and utils + for multiplexing frames. */ + +#include "fd_h2_rbuf.h" +#include "fd_h2_proto.h" + +/* fd_h2_settings_t contains HTTP/2 settings that fd_h2 understands. */ + +struct fd_h2_settings { + uint initial_window_size; + uint max_frame_size; + uint max_header_list_size; + uint max_concurrent_streams; +}; + +typedef struct fd_h2_settings fd_h2_settings_t; + +/* FD_H2_MAX_PENDING_SETTINGS limits the number of SETTINGS frames that + fd_h2 can burst without an acknowledgement. Aborts the conn with a + TCP RST if fd_h2 or the peer pile on too many unacknowledged SETTINGS + frames. */ + +#define FD_H2_MAX_PENDING_SETTINGS 64 + +/* fd_h2_conn is a framing-layer HTTP/2 connection handle. + It implements RFC 9113 mandatory behavior, such as negotiating conn + settings with a peer. */ + +struct fd_h2_conn { + fd_h2_callbacks_t const * cb; + + union { /* arbitrary value for use by caller */ + void * ctx; + ulong memo; + }; + + fd_h2_settings_t self_settings; + fd_h2_settings_t peer_settings; + + uchar * tx_frame_p; /* in-progress frame: first byte in rbuf */ + ulong tx_payload_off; /* in-progress frame: cum sent byte cnt before payload */ + ulong rx_suppress; /* skip frame handlers until this RX offset */ + + uint rx_frame_rem; /* current RX frame: payload bytes remaining */ + uint rx_stream_id; /* current RX frame: stream ID */ + uint rx_stream_next; /* next unused RX stream ID */ + + uint rx_wnd_wmark; /* receive window refill threshold */ + uint rx_wnd_max; /* receive window max size */ + uint rx_wnd; /* receive window bytes remaining */ + + uint tx_stream_next; /* next unused TX stream ID */ + uint tx_wnd; /* transmit quota available */ + + uint stream_active_cnt[2]; /* currently active {rx,tx} streams */ + + ushort flags; /* bit set of FD_H2_CONN_FLAGS_* */ + uchar conn_error; + uchar setting_tx; /* no of sent SETTINGS frames pending their ACK */ + uchar rx_frame_flags; /* current RX frame: flags */ + uchar rx_pad_rem; /* current RX frame: pad bytes remaining */ + uchar ping_tx; /* no of sent PING frames pending their ACK */ +}; + +/* FD_H2_CONN_FLAGS_* give flags related to conn lifecycle */ + +#define FD_H2_CONN_FLAGS_LG_DEAD (0) /* conn has passed */ +#define FD_H2_CONN_FLAGS_LG_SEND_GOAWAY (1) /* send GOAWAY */ +#define FD_H2_CONN_FLAGS_LG_CONTINUATION (2) /* next frame must be CONTINUATION */ +#define FD_H2_CONN_FLAGS_LG_CLIENT_INITIAL (3) /* send preface, SETTINGS */ +#define FD_H2_CONN_FLAGS_LG_WAIT_SETTINGS_ACK_0 (4) /* wait for initial ACK of initial SETTINGS */ +#define FD_H2_CONN_FLAGS_LG_WAIT_SETTINGS_0 (5) /* wait for peer's initial SETTINGS */ +#define FD_H2_CONN_FLAGS_LG_SERVER_INITIAL (6) /* wait for client preface, then send settings */ +#define FD_H2_CONN_FLAGS_LG_WINDOW_UPDATE (7) /* send WINDOW_UPDATE */ + +#define FD_H2_CONN_FLAGS_DEAD ((uchar)( 1U<= conn.self_settings.max_frame_size. */ + +void +fd_h2_rx( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_rx, + fd_h2_rbuf_t * rbuf_tx, + uchar * scratch, + ulong scratch_sz, + fd_h2_callbacks_t const * cb ); + +/* fd_h2_tx_control writes out control messages to rbuf_out. Should be + called immediately when creating a conn or whenever a timer + expires. */ + +void +fd_h2_tx_control( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_tx, + fd_h2_callbacks_t const * cb ); + +/* fd_h2_tx_check_sz checks whether rbuf_tx has enough space to buffer + a frame for sending. frame_max is the max frame size to check for. + Returns 1 if the send buffer has enough space to hold a frame, 0 + otherwise. + + Note that the caller should separately do the following checks: + - TCP send window space (optional, if fast delivery is desired) + - HTTP/2 connection send window space + - HTTP/2 stream limit check + - HTTP/2 stream send window space */ + +static inline int +fd_h2_tx_check_sz( fd_h2_rbuf_t const * rbuf_tx, + ulong frame_max ) { + /* Calculate the wire size of a frame */ + ulong tot_sz = 9UL + frame_max; + /* Leave some room in rbuf_tx for control frames */ + ulong req_sz = tot_sz + 64UL; + return fd_h2_rbuf_free_sz( rbuf_tx ) >= req_sz; +} + +/* fd_h2_tx_prepare appends a partial frame header to rbuf_tx. Assumes + that rbuf_tx has enough space to write a frame header. On return, + the caller can start appending the actual payload to rbuf_tx. */ + +static inline void +fd_h2_tx_prepare( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_tx, + uint frame_type, + uint flags, + uint stream_id ) { + if( FD_UNLIKELY( conn->tx_frame_p ) ) { + FD_LOG_CRIT(( "Mismatched fd_h2_tx_prepare" )); + } + + conn->tx_frame_p = rbuf_tx->hi; + conn->tx_payload_off = rbuf_tx->hi_off + 9; + + fd_h2_frame_hdr_t hdr = { + .typlen = fd_h2_frame_typlen( frame_type, 0 ), + .flags = (uchar)flags, + .r_stream_id = fd_uint_bswap( stream_id ) + }; + fd_h2_rbuf_push( rbuf_tx, &hdr, sizeof(fd_h2_frame_hdr_t) ); +} + +/* fd_h2_tx_commit finishes up a HTTP/2 frame. */ + +static inline void +fd_h2_tx_commit( fd_h2_conn_t * conn, + fd_h2_rbuf_t const * rbuf_tx ) { + ulong off0 = conn->tx_payload_off; + ulong off1 = rbuf_tx->hi_off; + uchar * buf0 = rbuf_tx->buf0; + uchar * buf1 = rbuf_tx->buf1; + ulong bufsz = rbuf_tx->bufsz; + uchar * frame = conn->tx_frame_p; + uchar * sz0 = frame; + uchar * sz1 = frame+1; + uchar * sz2 = frame+2; + sz1 = sz1>=buf1 ? sz1-bufsz : sz1; + sz2 = sz2>=buf1 ? sz2-bufsz : sz2; + + if( FD_UNLIKELY( frame=buf1 ) ) { + FD_LOG_CRIT(( "Can't finish frame: rbuf_tx doesn't match" )); + } + + ulong write_sz = (ulong)( off1-off0 ); + /* FIXME check write_sz? */ + *sz0 = (uchar)( write_sz>>16 ); + *sz1 = (uchar)( write_sz>> 8 ); + *sz2 = (uchar)( write_sz ); + + conn->tx_frame_p = NULL; + conn->tx_payload_off = 0UL; +} + +/* fd_h2_tx is a slow streamlined variant of fd_h2_tx_{prepare,commit}. + This variant assumes that the frame paylaod is already available in + a separate buffer. */ + +static inline void +fd_h2_tx( fd_h2_rbuf_t * rbuf_tx, + uchar const * payload, + ulong payload_sz, + uint frame_type, + uint flags, + uint stream_id ) { + fd_h2_frame_hdr_t hdr = { + .typlen = fd_h2_frame_typlen( frame_type, payload_sz ), + .flags = (uchar)flags, + .r_stream_id = fd_uint_bswap( stream_id ) + }; + fd_h2_rbuf_push( rbuf_tx, &hdr, sizeof(fd_h2_frame_hdr_t) ); + fd_h2_rbuf_push( rbuf_tx, payload, payload_sz ); +} + +/* fd_h2_tx_ping attempts to enqueue a PING frame for sending. */ + +int +fd_h2_tx_ping( fd_h2_conn_t * conn, + fd_h2_rbuf_t * rbuf_tx ); + +static inline void +fd_h2_conn_error( fd_h2_conn_t * conn, + uint err_code ) { + /* Clear all other flags */ + conn->flags = FD_H2_CONN_FLAGS_SEND_GOAWAY; + conn->conn_error = (uchar)err_code; +} + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_waltz_h2_fd_h2_conn_h */ diff --git a/src/waltz/h2/fd_h2_hdr_match.c b/src/waltz/h2/fd_h2_hdr_match.c new file mode 100644 index 0000000000..ee6efce431 --- /dev/null +++ b/src/waltz/h2/fd_h2_hdr_match.c @@ -0,0 +1,149 @@ +#include "fd_h2_hdr_match.h" +#include "fd_hpack_private.h" + +FD_TL ulong fd_h2_hdr_match_seed; + +fd_h2_hdr_match_entry_t const fd_h2_hdr_match_entry_null = {0}; + +static fd_h2_hdr_match_entry_t * +fd_h2_hdr_matcher_insert1( fd_h2_hdr_match_entry_t * map, + int id, + char const * name, /* static lifetime */ + ulong name_len ) { + fd_h2_hdr_match_key_t key = { .hdr=name, .hdr_len=(ushort)name_len }; + fd_h2_hdr_match_entry_t * entry = fd_h2_hdr_map_insert( map, key ); + if( FD_UNLIKELY( !entry ) ) return NULL; + entry->id = (short)id; + return entry; +} + +fd_h2_hdr_matcher_t * +fd_h2_hdr_matcher_init( void * mem, + ulong seed ) { + + if( FD_UNLIKELY( !mem ) ) { + FD_LOG_WARNING(( "NULL mem" )); + return NULL; + } + if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)mem, alignof(fd_h2_hdr_matcher_t) ) ) ) { + FD_LOG_WARNING(( "misaligned mem" )); + return NULL; + } + + fd_h2_hdr_matcher_t * matcher = mem; + if( FD_UNLIKELY( !fd_h2_hdr_map_join( fd_h2_hdr_map_new( matcher->entry ) ) ) ) { + return NULL; + } + fd_h2_hdr_match_entry_t * map = matcher->entry; + matcher->seed = seed; + matcher->entry_cnt = 0UL; + + fd_h2_hdr_match_seed = seed; + + int last_header_id = 0; + for( ulong i=1UL; i<=61; i++ ) { + int header_id = (int)fd_h2_hpack_matcher[ i ]; + if( last_header_id==header_id ) continue; + char const * name = fd_hpack_static_table[ i ].entry; + ulong name_len = fd_hpack_static_table[ i ].name_len; + fd_h2_hdr_matcher_insert1( map, header_id, name, name_len ); + last_header_id = header_id; + } + fd_h2_hdr_matcher_insert1( map, FD_H2_SEC_WEBSOCKET_KEY, "sec-websocket-key", 17UL ); + fd_h2_hdr_matcher_insert1( map, FD_H2_SEC_WEBSOCKET_EXTENSIONS, "sec-websocket-extensions", 25UL ); + fd_h2_hdr_matcher_insert1( map, FD_H2_SEC_WEBSOCKET_PROTOCOL, "sec-websocket-protocol", 22UL ); + fd_h2_hdr_matcher_insert1( map, FD_H2_SEC_WEBSOCKET_ACCEPT, "sec-websocket-accept", 20UL ); + fd_h2_hdr_matcher_insert1( map, FD_H2_SEC_WEBSOCKET_VERSION, "sec-websocket-version", 21UL ); + + /* matcher->entry_cnt still 0, which is deliberate */ + return matcher; +} + +void * +fd_h2_hdr_matcher_fini( fd_h2_hdr_matcher_t * matcher ) { + return matcher; +} + +void +fd_h2_hdr_matcher_insert( fd_h2_hdr_matcher_t * matcher, + int id, + char const * name, /* static lifetime */ + ulong name_len ) { + if( FD_UNLIKELY( id<1 || id>SHORT_MAX ) ) { + FD_LOG_ERR(( "id %d out of bounds", id )); + } + if( FD_UNLIKELY( matcher->entry_cnt>=FD_H2_HDR_MATCH_MAX ) ) { + FD_LOG_ERR(( "too many header entries (%lu)", matcher->entry_cnt )); + } + if( FD_UNLIKELY( name_len==0 || name_len>USHORT_MAX ) ) { + FD_LOG_ERR(( "invalid name_len: %lu", name_len )); + } + fd_h2_hdr_match_seed = matcher->seed; + if( FD_UNLIKELY( !fd_h2_hdr_matcher_insert1( matcher->entry, id, name, name_len ) ) ) return; + matcher->entry_cnt++; +} + +char const __attribute__((aligned(16))) +fd_h2_hpack_matcher[ 62 ] = { + [ 1 ] = FD_H2_HDR_AUTHORITY, + [ 2 ] = FD_H2_HDR_METHOD, + [ 3 ] = FD_H2_HDR_METHOD, + [ 4 ] = FD_H2_HDR_PATH, + [ 5 ] = FD_H2_HDR_PATH, + [ 6 ] = FD_H2_HDR_SCHEME, + [ 7 ] = FD_H2_HDR_SCHEME, + [ 8 ] = FD_H2_HDR_STATUS, + [ 9 ] = FD_H2_HDR_STATUS, + [ 10 ] = FD_H2_HDR_STATUS, + [ 11 ] = FD_H2_HDR_STATUS, + [ 12 ] = FD_H2_HDR_STATUS, + [ 13 ] = FD_H2_HDR_STATUS, + [ 14 ] = FD_H2_HDR_STATUS, + [ 15 ] = FD_H2_HDR_ACCEPT_CHARSET, + [ 16 ] = FD_H2_HDR_ACCEPT_ENCODING, + [ 17 ] = FD_H2_HDR_ACCEPT_LANGUAGE, + [ 18 ] = FD_H2_HDR_ACCEPT_RANGES, + [ 19 ] = FD_H2_HDR_ACCEPT, + [ 20 ] = FD_H2_HDR_ACCESS_CONTROL_ALLOW_ORIGIN, + [ 21 ] = FD_H2_HDR_AGE, + [ 22 ] = FD_H2_HDR_ALLOW, + [ 23 ] = FD_H2_HDR_AUTHORIZATION, + [ 24 ] = FD_H2_HDR_CACHE_CONTROL, + [ 25 ] = FD_H2_HDR_CONTENT_DISPOSITION, + [ 26 ] = FD_H2_HDR_CONTENT_ENCODING, + [ 27 ] = FD_H2_HDR_CONTENT_LANGUAGE, + [ 28 ] = FD_H2_HDR_CONTENT_LENGTH, + [ 29 ] = FD_H2_HDR_CONTENT_LOCATION, + [ 30 ] = FD_H2_HDR_CONTENT_RANGE, + [ 31 ] = FD_H2_HDR_CONTENT_TYPE, + [ 32 ] = FD_H2_HDR_COOKIE, + [ 33 ] = FD_H2_HDR_DATE, + [ 34 ] = FD_H2_HDR_ETAG, + [ 35 ] = FD_H2_HDR_EXPECT, + [ 36 ] = FD_H2_HDR_EXPIRES, + [ 37 ] = FD_H2_HDR_FROM, + [ 38 ] = FD_H2_HDR_HOST, + [ 39 ] = FD_H2_HDR_IF_MATCH, + [ 40 ] = FD_H2_HDR_IF_MODIFIED_SINCE, + [ 41 ] = FD_H2_HDR_IF_NONE_MATCH, + [ 42 ] = FD_H2_HDR_IF_RANGE, + [ 43 ] = FD_H2_HDR_IF_UNMODIFIED_SINCE, + [ 44 ] = FD_H2_HDR_LAST_MODIFIED, + [ 45 ] = FD_H2_HDR_LINK, + [ 46 ] = FD_H2_HDR_LOCATION, + [ 47 ] = FD_H2_HDR_MAX_FORWARDS, + [ 48 ] = FD_H2_HDR_PROXY_AUTHENTICATE, + [ 49 ] = FD_H2_HDR_PROXY_AUTHORIZATION, + [ 50 ] = FD_H2_HDR_RANGE, + [ 51 ] = FD_H2_HDR_REFERER, + [ 52 ] = FD_H2_HDR_REFRESH, + [ 53 ] = FD_H2_HDR_RETRY_AFTER, + [ 54 ] = FD_H2_HDR_SERVER, + [ 55 ] = FD_H2_HDR_SET_COOKIE, + [ 56 ] = FD_H2_HDR_STRICT_TRANSPORT_SECURITY, + [ 57 ] = FD_H2_HDR_TRANSFER_ENCODING, + [ 58 ] = FD_H2_HDR_USER_AGENT, + [ 59 ] = FD_H2_HDR_VARY, + [ 60 ] = FD_H2_HDR_VIA, + [ 61 ] = FD_H2_HDR_WWW_AUTHENTICATE +}; diff --git a/src/waltz/h2/fd_h2_hdr_match.h b/src/waltz/h2/fd_h2_hdr_match.h new file mode 100644 index 0000000000..84a9a86458 --- /dev/null +++ b/src/waltz/h2/fd_h2_hdr_match.h @@ -0,0 +1,238 @@ +#ifndef HEADER_fd_src_waltz_fd_h2_hdr_match_h +#define HEADER_fd_src_waltz_fd_h2_hdr_match_h + +/* fd_h2_hdr_match.h provides utils for building lookup tables for HTTP + header names. + + Example usage: + + // Define a custom header ID + #define MYAPP_HDR_FOO 1 + + // Initialize a matcher + ulong seed; + static fd_h2_hdr_matcher_t matcher[1]; + fd_h2_hdr_matcher_init( matcher, seed ); + fd_h2_hdr_matcher_add_literal( matcher, MYAPP_HDR_FOO, "x-myapp-foo" ); + + // Usage + FD_TEST( fd_h2_hdr_match( matcher, ":authority", 10 )==FD_H2_HDR_AUTHORITY ); + FD_TEST( fd_h2_hdr_match( matcher, "x-myapp-foo", 11 )==MYAPP_HDR_FOO ); + + See test_h2_hdr_match.c for more examples. */ + +#include "../../ballet/siphash13/fd_siphash13.h" +#include "fd_hpack.h" + +/* Declare an open-addressed hash table mapping header name strings to + arbitrary IDs (usually constants provided by the user). */ + +struct __attribute__((packed)) fd_h2_hdr_match_key { + char const * hdr; + ushort hdr_len; +}; + +typedef struct fd_h2_hdr_match_key fd_h2_hdr_match_key_t; + +static inline int +fd_h2_hdr_match_key_eq( fd_h2_hdr_match_key_t k1, + fd_h2_hdr_match_key_t k2 ) { + return k1.hdr_len == k2.hdr_len && fd_memeq( k1.hdr, k2.hdr, k1.hdr_len ); +} + +struct __attribute__((aligned(16))) fd_h2_hdr_match_entry { + fd_h2_hdr_match_key_t key; + short id; + uint hash; +}; + +typedef struct fd_h2_hdr_match_entry fd_h2_hdr_match_entry_t; + +extern fd_h2_hdr_match_entry_t const fd_h2_hdr_match_entry_null; + +#define FD_H2_HDR_MATCH_LG_SLOT_CNT 9 /* 512 slots */ +#define FD_H2_HDR_MATCH_MAX 300 /* target 0.7 load factor */ + +/* FIXME hack to work around lack of hash seed support in fd_map.c */ +extern FD_TL ulong fd_h2_hdr_match_seed; + +#define MAP_NAME fd_h2_hdr_map +#define MAP_T fd_h2_hdr_match_entry_t +#define MAP_KEY_T fd_h2_hdr_match_key_t +#define MAP_KEY key +#define MAP_HASH_T uint +#define MAP_HASH hash +#define MAP_KEY_HASH(k) (uint)fd_siphash13_hash( k.hdr, k.hdr_len, fd_h2_hdr_match_seed, 0UL ) +#define MAP_MEMOIZE 1 +#define MAP_LG_SLOT_CNT FD_H2_HDR_MATCH_LG_SLOT_CNT +#define MAP_KEY_NULL (fd_h2_hdr_match_key_t){0} +#define MAP_KEY_INVAL(k) ((k).hdr==NULL) +#define MAP_KEY_EQUAL(k1,k2) fd_h2_hdr_match_key_eq( (k1),(k2) ) +#define MAP_KEY_EQUAL_IS_SLOW 1 +#include "../../util/tmpl/fd_map.c" + +/* A h2_hdr_matcher is used to build a hash table for common header + names. It is primarly designed for compile-time static lists of + headers that a user might be interested in. The user should not + insert arbitrary entries into the map. */ + +struct fd_h2_hdr_matcher { + fd_h2_hdr_match_entry_t entry[ 1<seed; + fd_h2_hdr_match_key_t key = { .hdr=name, .hdr_len=(ushort)name_len }; + fd_h2_hdr_match_entry_t const * entry = + fd_h2_hdr_map_query_const( matcher->entry, key, &fd_h2_hdr_match_entry_null ); + return (int)entry->id; +} + +FD_PROTOTYPES_END + +/* Define common header IDs (non-standard) */ + +// Group 1: HPACK table +#define FD_H2_HDR_UNKNOWN 0 // *** sentinel *** +#define FD_H2_HDR_AUTHORITY -1 // :authority +#define FD_H2_HDR_METHOD -2 // :method +#define FD_H2_HDR_PATH -3 // :path +#define FD_H2_HDR_SCHEME -4 // :scheme +#define FD_H2_HDR_STATUS -5 // :status +#define FD_H2_HDR_ACCEPT_CHARSET -6 // accept-charset +#define FD_H2_HDR_ACCEPT_ENCODING -7 // accept-encoding +#define FD_H2_HDR_ACCEPT_LANGUAGE -8 // accept-language +#define FD_H2_HDR_ACCEPT_RANGES -9 // accept-ranges +#define FD_H2_HDR_ACCEPT -10 // accept +#define FD_H2_HDR_ACCESS_CONTROL_ALLOW_ORIGIN -11 // access-control-allow-origin +#define FD_H2_HDR_AGE -12 // age +#define FD_H2_HDR_ALLOW -13 // allow +#define FD_H2_HDR_AUTHORIZATION -14 // authorization +#define FD_H2_HDR_CACHE_CONTROL -15 // cache-control +#define FD_H2_HDR_CONTENT_DISPOSITION -16 // content-disposition +#define FD_H2_HDR_CONTENT_ENCODING -17 // content-encoding +#define FD_H2_HDR_CONTENT_LANGUAGE -18 // content-language +#define FD_H2_HDR_CONTENT_LENGTH -19 // content-length +#define FD_H2_HDR_CONTENT_LOCATION -20 // content-location +#define FD_H2_HDR_CONTENT_RANGE -21 // content-range +#define FD_H2_HDR_CONTENT_TYPE -22 // content-type +#define FD_H2_HDR_COOKIE -23 // cookie +#define FD_H2_HDR_DATE -24 // date +#define FD_H2_HDR_ETAG -25 // etag +#define FD_H2_HDR_EXPECT -26 // expect +#define FD_H2_HDR_EXPIRES -27 // expires +#define FD_H2_HDR_FROM -28 // from +#define FD_H2_HDR_HOST -29 // host +#define FD_H2_HDR_IF_MATCH -30 // if-match +#define FD_H2_HDR_IF_MODIFIED_SINCE -31 // if-modified-since +#define FD_H2_HDR_IF_NONE_MATCH -32 // if-none-match +#define FD_H2_HDR_IF_RANGE -33 // if-range +#define FD_H2_HDR_IF_UNMODIFIED_SINCE -34 // if-unmodified-since +#define FD_H2_HDR_LAST_MODIFIED -35 // last-modified +#define FD_H2_HDR_LINK -36 // link +#define FD_H2_HDR_LOCATION -37 // location +#define FD_H2_HDR_MAX_FORWARDS -38 // max-forwards +#define FD_H2_HDR_PROXY_AUTHENTICATE -39 // proxy-authenticate +#define FD_H2_HDR_PROXY_AUTHORIZATION -40 // proxy-authorization +#define FD_H2_HDR_RANGE -41 // range +#define FD_H2_HDR_REFERER -42 // referer +#define FD_H2_HDR_REFRESH -43 // refresh +#define FD_H2_HDR_RETRY_AFTER -44 // retry-after +#define FD_H2_HDR_SERVER -45 // server +#define FD_H2_HDR_SET_COOKIE -46 // set-cookie +#define FD_H2_HDR_STRICT_TRANSPORT_SECURITY -47 // strict-transport-security +#define FD_H2_HDR_TRANSFER_ENCODING -48 // transfer-encoding +#define FD_H2_HDR_USER_AGENT -49 // user-agent +#define FD_H2_HDR_VARY -50 // vary +#define FD_H2_HDR_VIA -51 // via +#define FD_H2_HDR_WWW_AUTHENTICATE -52 // www-authenticate + +// Group 2: Other common +#define FD_H2_SEC_WEBSOCKET_KEY -53 // sec-websocket-key +#define FD_H2_SEC_WEBSOCKET_EXTENSIONS -54 // sec-websocket-extensions +#define FD_H2_SEC_WEBSOCKET_ACCEPT -55 // sec-websocket-accept +#define FD_H2_SEC_WEBSOCKET_PROTOCOL -56 // sec-websocket-protocol +#define FD_H2_SEC_WEBSOCKET_VERSION -57 // sec-websocket-version + +#endif /* HEADER_fd_src_waltz_fd_h2_hdr_match_h */ + diff --git a/src/waltz/h2/fd_h2_proto.c b/src/waltz/h2/fd_h2_proto.c new file mode 100644 index 0000000000..b2ccd4c585 --- /dev/null +++ b/src/waltz/h2/fd_h2_proto.c @@ -0,0 +1,57 @@ +#include "fd_h2_proto.h" + +FD_FN_CONST char const * +fd_h2_frame_name( uint frame_id ) { + switch( frame_id ) { + case FD_H2_FRAME_TYPE_DATA: return "DATA"; + case FD_H2_FRAME_TYPE_HEADERS: return "HEADERS"; + case FD_H2_FRAME_TYPE_PRIORITY: return "PRIORITY"; + case FD_H2_FRAME_TYPE_RST_STREAM: return "RST_STREAM"; + case FD_H2_FRAME_TYPE_SETTINGS: return "SETTINGS"; + case FD_H2_FRAME_TYPE_PUSH_PROMISE: return "PUSH_PROMISE"; + case FD_H2_FRAME_TYPE_PING: return "PING"; + case FD_H2_FRAME_TYPE_GOAWAY: return "GOAWAY"; + case FD_H2_FRAME_TYPE_WINDOW_UPDATE: return "WINDOW_UPDATE"; + case FD_H2_FRAME_TYPE_CONTINUATION: return "CONTINUATION"; + case FD_H2_FRAME_TYPE_ALTSVC: return "ALTSVC"; + case FD_H2_FRAME_TYPE_ORIGIN: return "ORIGIN"; + case FD_H2_FRAME_TYPE_PRIORITY_UPDATE: return "PRIORITY_UPDATE"; + default: + return "unknown"; + } +} + +FD_FN_CONST char const * +fd_h2_setting_name( uint setting_id ) { + switch( setting_id ) { + case 0: return "reserved"; + case FD_H2_SETTINGS_HEADER_TABLE_SIZE: return "HEADER_TABLE_SIZE"; + case FD_H2_SETTINGS_ENABLE_PUSH: return "ENABLE_PUSH"; + case FD_H2_SETTINGS_MAX_CONCURRENT_STREAMS: return "MAX_CONCURRENT_STREAMS"; + case FD_H2_SETTINGS_INITIAL_WINDOW_SIZE: return "INITIAL_WINDOW_SIZE"; + case FD_H2_SETTINGS_MAX_FRAME_SIZE: return "MAX_FRAME_SIZE"; + case FD_H2_SETTINGS_MAX_HEADER_LIST_SIZE: return "MAX_HEADER_LIST_SIZE"; + default: return "unknown"; + } +} + +FD_FN_CONST char const * +fd_h2_strerror( uint err ) { + switch( err ) { + case FD_H2_SUCCESS: return "success"; + case FD_H2_ERR_PROTOCOL: return "protocol error"; + case FD_H2_ERR_INTERNAL: return "internal error"; + case FD_H2_ERR_FLOW_CONTROL: return "flow control error"; + case FD_H2_ERR_SETTINGS_TIMEOUT: return "timed out waiting for settings"; + case FD_H2_ERR_STREAM_CLOSED: return "stream closed"; + case FD_H2_ERR_FRAME_SIZE: return "invalid frame size"; + case FD_H2_ERR_REFUSED_STREAM: return "stream refused"; + case FD_H2_ERR_CANCEL: return "stream cancelled"; + case FD_H2_ERR_COMPRESSION: return "compression error"; + case FD_H2_ERR_CONNECT: return "error while connecting"; + case FD_H2_ERR_ENHANCE_YOUR_CALM: return "enhance your calm"; + case FD_H2_ERR_INADEQUATE_SECURITY: return "inadequate security"; + case FD_H2_ERR_HTTP_1_1_REQUIRED: return "HTTP/1.1 required"; + default: return "unknown"; + } +} diff --git a/src/waltz/h2/fd_h2_proto.h b/src/waltz/h2/fd_h2_proto.h new file mode 100644 index 0000000000..b4e64cab7e --- /dev/null +++ b/src/waltz/h2/fd_h2_proto.h @@ -0,0 +1,169 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_h2_proto +#define HEADER_fd_src_waltz_h2_fd_h2_proto + +/* fd_h2_proto.h contains constants and data structures taken from + HTTP/2 specs. */ + +#include "fd_h2_base.h" + +/* FD_H2_FRAME_TYPE_* give HTTP/2 frame IDs. + https://www.iana.org/assignments/http2-parameters/http2-parameters.xhtml#frame-type */ + +#define FD_H2_FRAME_TYPE_DATA ((uchar)0x00) +#define FD_H2_FRAME_TYPE_HEADERS ((uchar)0x01) +#define FD_H2_FRAME_TYPE_PRIORITY ((uchar)0x02) +#define FD_H2_FRAME_TYPE_RST_STREAM ((uchar)0x03) +#define FD_H2_FRAME_TYPE_SETTINGS ((uchar)0x04) +#define FD_H2_FRAME_TYPE_PUSH_PROMISE ((uchar)0x05) +#define FD_H2_FRAME_TYPE_PING ((uchar)0x06) +#define FD_H2_FRAME_TYPE_GOAWAY ((uchar)0x07) +#define FD_H2_FRAME_TYPE_WINDOW_UPDATE ((uchar)0x08) +#define FD_H2_FRAME_TYPE_CONTINUATION ((uchar)0x09) +#define FD_H2_FRAME_TYPE_ALTSVC ((uchar)0x0a) +#define FD_H2_FRAME_TYPE_ORIGIN ((uchar)0x0c) +#define FD_H2_FRAME_TYPE_PRIORITY_UPDATE ((uchar)0x10) + +/* fd_h2_frame_{length,type} pack/unpack the typlen field as found in a + frame header. */ + +FD_FN_CONST static inline uchar +fd_h2_frame_type( uint typlen ) { + return (uchar)( typlen>>24 ); /* in [0,2^8) */ +} + +FD_FN_CONST static inline uint +fd_h2_frame_length( uint typlen ) { + return fd_uint_bswap( typlen<<8 ) & 0xFFFFFF; /* in [0,2^24) */ +} + +/* fd_h2_frame_typlen packs the typlen field for use in a frame header. */ + +FD_FN_CONST static inline uint +fd_h2_frame_typlen( ulong type, /* in [0,2^8) */ + ulong length ) { /* in [0,2^24) */ + return (fd_uint_bswap( (uint)length )>>8) | ((uint)type<<24); +} + +FD_FN_CONST static inline uint +fd_h2_frame_stream_id( uint r_stream_id ) { + return fd_uint_bswap( r_stream_id ) & 0x7fffffffu; +} + +/* fd_h2_frame_hdr_t matches the encoding of a HTTP/2 frame header. + https://www.rfc-editor.org/rfc/rfc9113.html#section-4.1 */ + +struct __attribute__((packed)) fd_h2_frame_hdr { + uint typlen; + uchar flags; + uint r_stream_id; +}; + +typedef struct fd_h2_frame_hdr fd_h2_frame_hdr_t; + +#define FD_H2_FLAG_ACK ((uchar)0x01) +#define FD_H2_FLAG_END_STREAM ((uchar)0x01) +#define FD_H2_FLAG_END_HEADERS ((uchar)0x04) +#define FD_H2_FLAG_PADDED ((uchar)0x08) +#define FD_H2_FLAG_PRIORITY ((uchar)0x20) + + +/* fd_h2_priority_t matches the encoding of a PRIORITY frame. */ + +struct __attribute__((packed)) fd_h2_priority { + fd_h2_frame_hdr_t hdr; + uint r_stream_dep; + uchar weight; +}; + +typedef struct fd_h2_priority fd_h2_priority_t; + + +/* A SETTINGS frame contains a series of fd_h2_setting_t. */ + +struct __attribute__((packed)) fd_h2_setting { + ushort id; + uint value; +}; + +typedef struct fd_h2_setting fd_h2_setting_t; + +/* FD_H2_SETTINGS_* give HTTP/2 setting IDs. + https://www.iana.org/assignments/http2-parameters/http2-parameters.xhtml#settings + https://www.rfc-editor.org/rfc/rfc9113.html#section-6.5.2 */ + +#define FD_H2_SETTINGS_HEADER_TABLE_SIZE ((ushort)0x01) +#define FD_H2_SETTINGS_ENABLE_PUSH ((ushort)0x02) +#define FD_H2_SETTINGS_MAX_CONCURRENT_STREAMS ((ushort)0x03) +#define FD_H2_SETTINGS_INITIAL_WINDOW_SIZE ((ushort)0x04) +#define FD_H2_SETTINGS_MAX_FRAME_SIZE ((ushort)0x05) +#define FD_H2_SETTINGS_MAX_HEADER_LIST_SIZE ((ushort)0x06) + + +/* fd_h2_ping_t matches the encoding of a PING frame. + https://www.rfc-editor.org/rfc/rfc9113.html#name-ping + Valid flags: ACK */ + +struct __attribute__((packed)) fd_h2_ping { + fd_h2_frame_hdr_t hdr; + + ulong payload; +}; + +typedef struct fd_h2_ping fd_h2_ping_t; + + +/* fd_h2_goaway_t matches the encoding of a GOAWAY frame. + https://www.rfc-editor.org/rfc/rfc9113.html#name-goaway */ + +struct __attribute__((packed)) fd_h2_goaway { + fd_h2_frame_hdr_t hdr; + + uint last_stream_id; + uint error_code; + /* variable length debug data follows ... */ +}; + +typedef struct fd_h2_goaway fd_h2_goaway_t; + + +/* fd_h2_window_update_t matches the encoding of a WINDOW_UPDATE frame. + https://www.rfc-editor.org/rfc/rfc9113.html#name-window_update */ + +struct __attribute__((packed)) fd_h2_window_update { + fd_h2_frame_hdr_t hdr; + + uint increment; +}; + +typedef struct fd_h2_window_update fd_h2_window_update_t; + + +/* fd_h2_rst_stream_t matches the encoding of a RST_STREAM frame. + https://www.rfc-editor.org/rfc/rfc9113.html#name-rst_stream */ + +struct __attribute__((packed)) fd_h2_rst_stream { + fd_h2_frame_hdr_t hdr; + + uint error_code; +}; + +typedef struct fd_h2_rst_stream fd_h2_rst_stream_t; + + +FD_PROTOTYPES_BEGIN + +/* fd_h2_frame_name returns a static-lifetime uppercase cstr with the + name of a HTTP/2 frame. */ + +FD_FN_CONST char const * +fd_h2_frame_name( uint frame_id ); + +/* fd_h2_setting_name returns a static-lifetime uppercase cstr with the + name of a HTTP/2 setting. */ + +FD_FN_CONST char const * +fd_h2_setting_name( uint setting_id ); + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_waltz_h2_fd_h2_proto */ diff --git a/src/waltz/h2/fd_h2_rbuf.h b/src/waltz/h2/fd_h2_rbuf.h new file mode 100644 index 0000000000..1115f1454d --- /dev/null +++ b/src/waltz/h2/fd_h2_rbuf.h @@ -0,0 +1,242 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_h2_rbuf_h +#define HEADER_fd_src_waltz_h2_fd_h2_rbuf_h + +/* fd_h2_rbuf.h provides a byte oriented unaligend ring buffer. */ + +#include "fd_h2_base.h" +#include "../../util/log/fd_log.h" + +struct fd_h2_rbuf { + uchar * buf0; /* points to first byte of buffer */ + uchar * buf1; /* points one past last byte of buffer */ + uchar * lo; /* in [buf0,buf1) */ + uchar * hi; /* in [buf0,buf1) */ + ulong lo_off; + ulong hi_off; + ulong bufsz; +}; + +FD_PROTOTYPES_BEGIN + +/* fd_h2_rbuf_init initializes an h2_rbuf backed by the given buffer. + On return, h2_rbuf has a read-write interested in buf. bufsz has no + alignment requirements. */ + +static inline fd_h2_rbuf_t * +fd_h2_rbuf_init( fd_h2_rbuf_t * rbuf, + void * buf, + ulong bufsz ) { + *rbuf = (fd_h2_rbuf_t) { + .buf0 = (uchar *)buf, + .buf1 = (uchar *)buf+bufsz, + .lo = (uchar *)buf, + .hi = (uchar *)buf, + .bufsz = bufsz + }; + return rbuf; +} + +/* fd_h2_rbuf_used_sz returns the number of unconsumed bytes in rbuf. */ + +FD_FN_PURE static inline ulong +fd_h2_rbuf_used_sz( fd_h2_rbuf_t const * rbuf ) { + return rbuf->hi_off - rbuf->lo_off; +} + +/* fd_h2_rbuf_free_sz returns the number of bytes that can be appended + using fd_h2_rbuf_push. */ + +FD_FN_PURE static inline ulong +fd_h2_rbuf_free_sz( fd_h2_rbuf_t const * rbuf ) { + long used = (long)fd_h2_rbuf_used_sz( rbuf ); + return (ulong)fd_long_max( 0L, rbuf->buf1 - rbuf->buf0 - used ); +} + +/* fd_h2_rbuf_push appends a series of newly received bytes into rbuf. + Returns chunk_sz. + + WARNING: The caller must not pass a chunk_sz larger than + fd_h2_rbuf_free_sz bytes. */ + +static inline void +fd_h2_rbuf_push( fd_h2_rbuf_t * rbuf, + void const * chunk, + ulong chunk_sz ) { + uchar * buf0 = rbuf->buf0; + uchar * buf1 = rbuf->buf1; + uchar * lo = rbuf->lo; + uchar * hi = rbuf->hi; + rbuf->hi_off += chunk_sz; + + if( FD_UNLIKELY( hi+chunk_sz > rbuf->buf1 ) ) { + /* Split copy */ + if( FD_UNLIKELY( lo>hi ) ) { + FD_LOG_CRIT(( "rbuf overflow: buf_sz=%lu lo=%ld hi=%ld chunk_sz=%lu", + rbuf->bufsz, rbuf->lo-buf0, rbuf->hi-buf0, chunk_sz )); + } + ulong part1 = (ulong)( buf1-hi ); + ulong part2 = (ulong)( chunk_sz-part1 ); + fd_memcpy( hi, chunk, part1 ); + fd_memcpy( buf0, (void *)( (ulong)chunk+part1 ), part2 ); + rbuf->hi = buf0+part2; + return; + } + + /* One-shot copy */ + uchar * new_hi = hi+chunk_sz; + if( new_hi==buf1 ) new_hi = buf0; + fd_memcpy( hi, chunk, chunk_sz ); + rbuf->hi = new_hi; + return; +} + +/* fd_h2_rbuf_peek_used returns a pointer to the first contiguous + fragment of unconsumed data. *sz is set to the number of contiguous + bytes starting at rbuf->lo. *split_sz is set to the number of bytes + that are unconsumed, but in a separate fragment. The caller may + mangle bytes in [retval,retval+sz) if it consumes these bytes + immediately afterwards. */ + +static inline uchar * +fd_h2_rbuf_peek_used( fd_h2_rbuf_t * rbuf, + ulong * sz, + ulong * split_sz ) { + ulong used_sz = fd_h2_rbuf_used_sz( rbuf ); + uchar * buf0 = rbuf->buf0; + uchar * buf1 = rbuf->buf1; + uchar * lo = rbuf->lo; + uchar * hi = rbuf->hi; + uchar * end = lo+used_sz; + /* FIXME make this branchless */ + if( end<=buf1 ) { + *sz = (ulong)( hi - lo ); + *split_sz = 0UL; + } else { + *sz = (ulong)( buf1 - lo ); + *split_sz = (ulong)( hi - buf0 ); + } + return lo; +} + +/* fd_h2_rbuf_peek_free is like fd_h2_rbuf_peek_used, but refers to the + free region. */ + +static inline uchar * +fd_h2_rbuf_peek_free( fd_h2_rbuf_t * rbuf, + ulong * sz, + ulong * split_sz ) { + ulong free_sz = fd_h2_rbuf_free_sz( rbuf ); + uchar * buf0 = rbuf->buf0; + uchar * buf1 = rbuf->buf1; + uchar * lo = rbuf->lo; + uchar * hi = rbuf->hi; + uchar * end = hi+free_sz; + /* FIXME make this branchless */ + if( end<=buf1 ) { + *sz = (ulong)( buf1 - hi ); + *split_sz = 0UL; + } else { + *sz = (ulong)( buf1 - hi ); + *split_sz = (ulong)( buf0 - lo ); + } + return hi; +} + +/* fd_h2_rbuf_skip frees n bytes from rbuf. Freeing more bytes than + returned by fd_h2_rbuf_used_sz corrupts the buffer state. */ + +static inline void +fd_h2_rbuf_skip( fd_h2_rbuf_t * rbuf, + ulong n ) { + uchar * lo = rbuf->lo; + ulong bufsz = rbuf->bufsz; + uchar * buf1 = rbuf->buf1; + rbuf->lo_off += n; + lo += n; + if( FD_UNLIKELY( lo>=buf1 ) ) { + lo -= bufsz; + } + rbuf->lo = lo; +} + +/* fd_h2_rbuf_alloc marks the next n free bytes as used. */ + +static inline void +fd_h2_rbuf_alloc( fd_h2_rbuf_t * rbuf, + ulong n ) { + uchar * hi = rbuf->hi; + ulong bufsz = rbuf->bufsz; + uchar * buf1 = rbuf->buf1; + rbuf->hi_off += n; + hi += n; + if( FD_UNLIKELY( hi>=buf1 ) ) { + hi -= bufsz; + } + rbuf->hi = hi; +} + +/* fd_h2_rbuf_pop consumes n bytes from rbuf. n is the number of bytes + to consume. n is assumed to be <= fd_h2_rbuf_used(rbuf). scratch + points to scratch memory with space for n bytes. + + If the bytes are available contiguously in rbuf, returns a pointer to + them. Otherwise, the bytes are copied into scratch. The returned + pointer is valid until the next mutating rbuf operation. */ + +static inline uchar * +fd_h2_rbuf_pop( fd_h2_rbuf_t * rbuf, + uchar * scratch, + ulong n ) { + uchar * lo = rbuf->lo; + uchar * buf0 = rbuf->buf0; + uchar * buf1 = rbuf->buf1; + ulong bufsz = rbuf->bufsz; + uchar * ret = lo; + rbuf->lo_off += n; + uchar * end = lo+n; + if( FD_UNLIKELY( (lo+n)>=buf1 ) ) { + end -= bufsz; + } + if( FD_UNLIKELY( (lo+n)>buf1 ) ) { + ulong part0 = (ulong)( buf1-lo ); + ulong part1 = n-part0; + fd_memcpy( scratch, lo, part0 ); + fd_memcpy( scratch+part0, buf0, part1 ); + ret = scratch; + } + rbuf->lo = end; + return ret; +} + +static inline void +fd_h2_rbuf_pop_copy( fd_h2_rbuf_t * rbuf, + void * out, + ulong n ) { + uchar * lo = rbuf->lo; + uchar * buf0 = rbuf->buf0; + uchar * buf1 = rbuf->buf1; + ulong bufsz = rbuf->bufsz; + rbuf->lo_off += n; + uchar * end = lo+n; + if( FD_UNLIKELY( (lo+n)>=buf1 ) ) { + end -= bufsz; + } + if( FD_UNLIKELY( (lo+n)>buf1 ) ) { + ulong part0 = (ulong)( buf1-lo ); + ulong part1 = n-part0; + fd_memcpy( out, lo, part0 ); + fd_memcpy( (void *)( (ulong)out+part0 ), buf0, part1 ); + } else { + fd_memcpy( out, lo, n ); + } + rbuf->lo = end; +} + +FD_FN_PURE static inline int +fd_h2_rbuf_is_empty( fd_h2_rbuf_t const * rbuf ) { + return rbuf->lo_off==rbuf->hi_off; +} + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_waltz_h2_fd_h2_rbuf_h */ diff --git a/src/waltz/h2/fd_h2_rbuf_ossl.h b/src/waltz/h2/fd_h2_rbuf_ossl.h new file mode 100644 index 0000000000..03f353e2c3 --- /dev/null +++ b/src/waltz/h2/fd_h2_rbuf_ossl.h @@ -0,0 +1,58 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_h2_rbuf_ossl_h +#define HEADER_fd_src_waltz_h2_fd_h2_rbuf_ossl_h + +/* fd_h2_rbuf_ossl.h provides utils for I/O between rbuf and OpenSSL + BIO. */ + +#include "fd_h2_rbuf.h" + +#if FD_HAS_OPENSSL + +#include +#include + +/* fd_h2_rbuf_ssl_read reads bytes from a SSL and places them into rbuf. */ + +static inline ulong +fd_h2_rbuf_ssl_read( fd_h2_rbuf_t * rbuf_out, + SSL * ssl, + int * ssl_err ) { + ulong sz0, sz1; + uchar * rbuf_free = fd_h2_rbuf_peek_free( rbuf_out, &sz0, &sz1 ); + if( FD_UNLIKELY( !sz0 ) ) return 0UL; + + ERR_clear_error(); + ulong read_sz; + if( FD_UNLIKELY( !SSL_read_ex( ssl, rbuf_free, sz0, &read_sz ) ) ) { + *ssl_err = SSL_get_error( ssl, 0 ); + return 0UL; + } + fd_h2_rbuf_alloc( rbuf_out, read_sz ); + return read_sz; +} + +/* fd_h2_rbuf_ssl_write writes bytes from an rbuf into a SSL. + FIXME react to fatal errors here? */ + +static inline ulong +fd_h2_rbuf_ssl_write( fd_h2_rbuf_t * rbuf_in, + SSL * ssl ) { + ulong sz0, sz1; + uchar * rbuf_used = fd_h2_rbuf_peek_used( rbuf_in, &sz0, &sz1 ); + if( FD_UNLIKELY( !sz0 ) ) return 0UL; + + ulong write_sz; + if( FD_UNLIKELY( !SSL_write_ex( ssl, rbuf_used, sz0, &write_sz ) ) ) return 0UL; + if( FD_UNLIKELY( sz1 && write_sz==sz0 ) ) { + ulong write_sz1; + if( SSL_write_ex( ssl, rbuf_in->buf0, sz1, &write_sz1 ) ) { + write_sz += write_sz1; + } + } + fd_h2_rbuf_skip( rbuf_in, write_sz ); + return write_sz; +} + +#endif /* FD_HAS_OPENSSL */ + +#endif /* HEADER_fd_src_waltz_h2_fd_h2_rbuf_ossl_h */ diff --git a/src/waltz/h2/fd_h2_rbuf_sock.h b/src/waltz/h2/fd_h2_rbuf_sock.h new file mode 100644 index 0000000000..00eeede6fc --- /dev/null +++ b/src/waltz/h2/fd_h2_rbuf_sock.h @@ -0,0 +1,152 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_h2_rbuf_sock_h +#define HEADER_fd_src_waltz_h2_fd_h2_rbuf_sock_h + +#include "fd_h2_rbuf.h" + +#if FD_H2_HAS_SOCKETS + +#include +#include + +static inline ulong +fd_h2_rbuf_prepare_recvmsg( fd_h2_rbuf_t * rbuf, + struct iovec iov[2] ) { + uchar * buf0 = rbuf->buf0; + uchar * buf1 = rbuf->buf1; + uchar * lo = rbuf->lo; + uchar * hi = rbuf->hi; + ulong free_sz = fd_h2_rbuf_free_sz( rbuf ); + if( FD_UNLIKELY( !free_sz ) ) return 0UL; + + if( lo<=hi ) { + + iov[ 0 ].iov_base = hi; + iov[ 0 ].iov_len = fd_ulong_min( (ulong)( buf1-hi ), free_sz ); + free_sz -= iov[ 0 ].iov_len; + iov[ 1 ].iov_base = buf0; + iov[ 1 ].iov_len = fd_ulong_min( (ulong)( lo-buf0 ), free_sz ); + return 2UL; + + } else { + + iov[ 0 ].iov_base = hi; + iov[ 0 ].iov_len = free_sz; + iov[ 1 ].iov_base = NULL; + iov[ 1 ].iov_len = 0UL; + return 1uL; + + } +} + +static inline void +fd_h2_rbuf_commit_recvmsg( fd_h2_rbuf_t * rbuf, + struct iovec const iovec[2], + ulong sz ) { + uchar * buf0 = rbuf->buf0; + uchar * buf1 = rbuf->buf1; + struct iovec iov0 = iovec[0]; + struct iovec iov1 = iovec[1]; + rbuf->hi_off += sz; + if( sz > iov0.iov_len ) { + rbuf->hi = (uchar *)iov1.iov_base + ( sz - iov0.iov_len ); + } else { + rbuf->hi = (uchar *)iov0.iov_base + sz; + } + if( rbuf->hi == buf1 ) rbuf->hi = buf0; /* cmov */ +} + +static inline int +fd_h2_rbuf_recvmsg( fd_h2_rbuf_t * rbuf, + int sock, + int flags ) { + struct iovec iov[2]; + ulong iov_cnt = fd_h2_rbuf_prepare_recvmsg( rbuf, iov ); + if( FD_UNLIKELY( !iov_cnt ) ) return 0; + + struct msghdr msg = { + .msg_iov = iov, + .msg_iovlen = iov_cnt + }; + ssize_t sz = recvmsg( sock, &msg, flags ); + if( sz<0 ) { + if( FD_LIKELY( errno==EAGAIN ) ) return 0; + return errno; + } else if( FD_UNLIKELY( sz==0 ) ) { + return EPIPE; + } + + fd_h2_rbuf_commit_recvmsg( rbuf, iov, (ulong)sz ); + return 0; +} + +static inline ulong +fd_h2_rbuf_prepare_sendmsg( fd_h2_rbuf_t * rbuf, + struct iovec iov[2] ) { + uchar * buf0 = rbuf->buf0; + uchar * buf1 = rbuf->buf1; + uchar * lo = rbuf->lo; + uchar * hi = rbuf->hi; + ulong used_sz = fd_h2_rbuf_used_sz( rbuf ); + if( FD_UNLIKELY( !used_sz ) ) return 0UL; + + if( hi<=lo ) { + + iov[ 0 ].iov_base = lo; + iov[ 0 ].iov_len = fd_ulong_min( (ulong)( buf1-lo ), used_sz ); + used_sz -= iov[ 0 ].iov_len; + iov[ 1 ].iov_base = buf0; + iov[ 1 ].iov_len = fd_ulong_min( (ulong)( hi-buf0 ), used_sz ); + return 2UL; + + } else { + + iov[ 0 ].iov_base = lo; + iov[ 0 ].iov_len = used_sz; + iov[ 1 ].iov_base = NULL; + iov[ 1 ].iov_len = 0UL; + return 1uL; + + } +} + +static inline void +fd_h2_rbuf_commit_sendmsg( fd_h2_rbuf_t * rbuf, + struct iovec const iovec[2], + ulong sz ) { + uchar * buf0 = rbuf->buf0; + uchar * buf1 = rbuf->buf1; + struct iovec iov0 = iovec[0]; + struct iovec iov1 = iovec[1]; + rbuf->lo_off += sz; + if( sz > iov0.iov_len ) { + rbuf->lo = (uchar *)iov1.iov_base + ( sz - iov0.iov_len ); + } else { + rbuf->lo = (uchar *)iov0.iov_base + sz; + } + if( rbuf->lo == buf1 ) rbuf->lo = buf0; /* cmov */ +} + +static inline int +fd_h2_rbuf_sendmsg( fd_h2_rbuf_t * rbuf, + int sock, + int flags ) { + struct iovec iov[2]; + ulong iov_cnt = fd_h2_rbuf_prepare_sendmsg( rbuf, iov ); + if( FD_UNLIKELY( !iov_cnt ) ) return 0; + + struct msghdr msg = { + .msg_iov = iov, + .msg_iovlen = iov_cnt + }; + ssize_t sz = sendmsg( sock, &msg, flags ); + if( sz<0 ) { + return errno; + } + + fd_h2_rbuf_commit_sendmsg( rbuf, iov, (ulong)sz ); + return 0; +} + +#endif /* FD_H2_HAS_SOCKETS */ + +#endif /* HEADER_fd_src_waltz_h2_fd_h2_rbuf_sock_h */ diff --git a/src/waltz/h2/fd_h2_stream.h b/src/waltz/h2/fd_h2_stream.h new file mode 100644 index 0000000000..eb05937ddd --- /dev/null +++ b/src/waltz/h2/fd_h2_stream.h @@ -0,0 +1,164 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_h2_stream_h +#define HEADER_fd_src_waltz_h2_fd_h2_stream_h + +/* fd_h2_stream.h provides the HTTP/2 stream state machine. */ + +#include "fd_h2_base.h" +#include "fd_h2_proto.h" +#include "fd_h2_conn.h" + +/* The fd_h2_stream_t object holds the stream state machine. */ + +struct fd_h2_stream { + uint stream_id; + uint tx_wnd; /* transmit quota available */ + uint rx_wnd; /* receive window bytes remaining */ + + uchar state; + uchar hdrs_seq; +}; + +#define FD_H2_STREAM_STATE_IDLE 0 +#define FD_H2_STREAM_STATE_OPEN 1 +#define FD_H2_STREAM_STATE_CLOSING_TX 2 /* half-closed (local) */ +#define FD_H2_STREAM_STATE_CLOSING_RX 3 /* half-closed (remote) */ +#define FD_H2_STREAM_STATE_CLOSED 4 +#define FD_H2_STREAM_STATE_ILLEGAL 5 + +FD_PROTOTYPES_BEGIN + +/* fd_h2_stream_init initializes a stream object. On return, the stream + is in 'IDLE' state and does not have an assigned stream ID. */ + +static inline fd_h2_stream_t * +fd_h2_stream_init( fd_h2_stream_t * stream ) { + *stream = (fd_h2_stream_t){0}; + return stream; +} + +/* fd_h2_stream_open transitions a stream from 'IDLE' to 'OPEN'. + In fd_h2, this happens when the local or peer side sends a HEADERS + frame. The TX side of the stream assumes the peer's default send + window. */ + +static inline fd_h2_stream_t * +fd_h2_stream_open( fd_h2_stream_t * stream, + fd_h2_conn_t const * conn, + uint stream_id ) { + *stream = (fd_h2_stream_t) { + .stream_id = stream_id, + .state = FD_H2_STREAM_STATE_OPEN, + .tx_wnd = conn->peer_settings.initial_window_size, + .rx_wnd = conn->self_settings.initial_window_size, + .hdrs_seq = 0U + }; + return stream; +} + +static inline void +fd_h2_stream_error1( fd_h2_rbuf_t * rbuf_tx, + uint stream_id, + uint h2_err ) { + fd_h2_rst_stream_t rst_stream = { + .hdr = { + .typlen = fd_h2_frame_typlen( FD_H2_FRAME_TYPE_RST_STREAM, 4UL ), + .flags = 0U, + .r_stream_id = fd_uint_bswap( stream_id ) + }, + .error_code = fd_uint_bswap( h2_err ) + }; + fd_h2_rbuf_push( rbuf_tx, &rst_stream, sizeof(fd_h2_rst_stream_t) ); +} + +static inline void +fd_h2_stream_error( fd_h2_stream_t * stream, + fd_h2_rbuf_t * rbuf_tx, + uint h2_err ) { + fd_h2_stream_error1( rbuf_tx, stream->stream_id, h2_err ); + stream->state = FD_H2_STREAM_STATE_CLOSED; +} + +static inline void +fd_h2_stream_private_deactivate( fd_h2_stream_t * stream, + fd_h2_conn_t * conn ) { + conn->stream_active_cnt[ (stream->stream_id&1) ^ (conn->rx_stream_id&1) ]--; +} + +static inline void +fd_h2_stream_close_rx( fd_h2_stream_t * stream, + fd_h2_conn_t * conn ) { + switch( stream->state ) { + case FD_H2_STREAM_STATE_OPEN: + stream->state = FD_H2_STREAM_STATE_CLOSING_RX; + break; + case FD_H2_STREAM_STATE_CLOSING_TX: + stream->state = FD_H2_STREAM_STATE_CLOSED; + fd_h2_stream_private_deactivate( stream, conn ); + break; + default: + stream->state = FD_H2_STREAM_STATE_ILLEGAL; + break; + } +} + +static inline void +fd_h2_stream_close_tx( fd_h2_stream_t * stream, + fd_h2_conn_t * conn ) { + switch( stream->state ) { + case FD_H2_STREAM_STATE_OPEN: + stream->state = FD_H2_STREAM_STATE_CLOSING_TX; + break; + case FD_H2_STREAM_STATE_CLOSING_RX: + stream->state = FD_H2_STREAM_STATE_CLOSED; + fd_h2_stream_private_deactivate( stream, conn ); + break; + default: + stream->state = FD_H2_STREAM_STATE_ILLEGAL; + break; + } +} + +static inline void +fd_h2_stream_reset( fd_h2_stream_t * stream, + fd_h2_conn_t * conn ) { + switch( stream->state ) { + case FD_H2_STREAM_STATE_OPEN: + case FD_H2_STREAM_STATE_CLOSING_TX: + case FD_H2_STREAM_STATE_CLOSING_RX: + stream->state = FD_H2_STREAM_STATE_CLOSED; + fd_h2_stream_private_deactivate( stream, conn ); + break; + default: + stream->state = FD_H2_STREAM_STATE_ILLEGAL; + break; + } +} + +static inline void +fd_h2_stream_rx_headers( fd_h2_stream_t * stream, + fd_h2_conn_t * conn, + ulong flags ) { + if( stream->state == FD_H2_STREAM_STATE_IDLE ) { + /* FIXME This is probably redundant */ + stream->state = FD_H2_STREAM_STATE_OPEN; + } + if( flags & FD_H2_FLAG_END_STREAM ) { + fd_h2_stream_close_rx( stream, conn ); + } + if( flags & FD_H2_FLAG_END_HEADERS ) { + stream->hdrs_seq = (uchar)( stream->hdrs_seq + 1 ); + } +} + +static inline void +fd_h2_stream_rx_data( fd_h2_stream_t * stream, + fd_h2_conn_t * conn, + ulong flags ) { + if( flags & FD_H2_FLAG_END_STREAM ) { + fd_h2_stream_close_rx( stream, conn ); + } +} + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_waltz_h2_fd_h2_stream_h */ diff --git a/src/waltz/h2/fd_h2_tx.c b/src/waltz/h2/fd_h2_tx.c new file mode 100644 index 0000000000..4e01aa9ee3 --- /dev/null +++ b/src/waltz/h2/fd_h2_tx.c @@ -0,0 +1,48 @@ +#include "fd_h2_tx.h" +#include "fd_h2_conn.h" +#include "fd_h2_stream.h" + +void +fd_h2_tx_op_copy( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + fd_h2_rbuf_t * rbuf_tx, + fd_h2_tx_op_t * tx_op ) { + long quota = fd_long_min( conn->tx_wnd, stream->tx_wnd ); + if( FD_UNLIKELY( quota<0L ) ) return; + + if( FD_UNLIKELY( stream->state == FD_H2_STREAM_STATE_CLOSED ) ) return; + if( FD_UNLIKELY( stream->state != FD_H2_STREAM_STATE_OPEN && + stream->state != FD_H2_STREAM_STATE_CLOSING_RX ) ) { + return; + } + + do { + /* Calculate how much we can send in this frame */ + long const rem_sz = (long)tx_op->chunk_sz; + long const buf_spc = (long)fd_h2_rbuf_free_sz( rbuf_tx ) - (long)sizeof(fd_h2_frame_hdr_t); + long const frame_max = (long)conn->peer_settings.max_frame_size; + + long payload_sz = fd_long_min( quota, rem_sz ); + /**/ payload_sz = fd_long_min( payload_sz, buf_spc ); + /**/ payload_sz = fd_long_min( payload_sz, frame_max ); + if( FD_UNLIKELY( payload_sz<=0L ) ) break; + long const next_rem_sz = rem_sz-payload_sz; + + /* END_STREAM flag */ + uint flags = 0U; + if( (next_rem_sz==0) & (!!tx_op->fin) ) { + flags |= FD_H2_FLAG_END_STREAM; + fd_h2_stream_close_tx( stream, conn ); + } + + fd_h2_tx_prepare( conn, rbuf_tx, FD_H2_FRAME_TYPE_DATA, flags, stream->stream_id ); + fd_h2_rbuf_push( rbuf_tx, tx_op->chunk, (ulong)payload_sz ); + fd_h2_tx_commit( conn, rbuf_tx ); + + tx_op->chunk = (void *)( (ulong)tx_op->chunk + (ulong)payload_sz ); + tx_op->chunk_sz = (ulong)next_rem_sz; + conn->tx_wnd -= (uint)payload_sz; + stream->tx_wnd -= (uint)payload_sz; + quota -= payload_sz; + } while( quota ); +} diff --git a/src/waltz/h2/fd_h2_tx.h b/src/waltz/h2/fd_h2_tx.h new file mode 100644 index 0000000000..f360b1ae09 --- /dev/null +++ b/src/waltz/h2/fd_h2_tx.h @@ -0,0 +1,53 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_h2_tx_h +#define HEADER_fd_src_waltz_h2_fd_h2_tx_h + +/* fd_h2_tx.h helps doing flow-controlled TX work. */ + +#include "fd_h2_proto.h" + +struct fd_h2_tx_op { + void const * chunk; + ulong chunk_sz; + ulong fin : 1; +}; + +typedef struct fd_h2_tx_op fd_h2_tx_op_t; + +FD_PROTOTYPES_BEGIN + +/* fd_h2_tx_op_init starts a send operation of chunk_sz bytes. chunk + points to the buffer to send. flags==FD_H2_FLAG_END_STREAM closes + the stream once all DATA frames have been generated. chunk_sz must + be non-zero. */ + +static inline fd_h2_tx_op_t * +fd_h2_tx_op_init( fd_h2_tx_op_t * tx_op, + void const * chunk, + ulong chunk_sz, + uint flags ) { + *tx_op = (fd_h2_tx_op_t) { + .chunk = chunk, + .chunk_sz = chunk_sz + }; + if( flags & FD_H2_FLAG_END_STREAM ) tx_op->fin = 1; + return tx_op; +} + +/* fd_h2_tx_op_copy copies as much enqueued tx data out to rbuf_tx as + possible. Advances tx_op->chunk and reduces tx_op->chunk_sz. Stops + copying when one of the following limits is hit: {rbuf_tx is full, + out of conn TX quota, out of stream TX quota, no more tx_op bytes + queued}. */ + +void +fd_h2_tx_op_copy( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + fd_h2_rbuf_t * rbuf_tx, + fd_h2_tx_op_t * tx_op ); + +/* FIXME Add sendmsg-gather API here for streamlined transmit of + multiple frames via sockets? */ + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_waltz_h2_fd_h2_tx_h */ diff --git a/src/waltz/h2/fd_hpack.c b/src/waltz/h2/fd_hpack.c new file mode 100644 index 0000000000..8ca26b1114 --- /dev/null +++ b/src/waltz/h2/fd_hpack.c @@ -0,0 +1,247 @@ +#include "fd_hpack.h" +#include "fd_h2_base.h" +#include "fd_hpack_private.h" +#include "nghttp2_hd_huffman.h" +#include "../../util/log/fd_log.h" + +fd_hpack_static_entry_t const +fd_hpack_static_table[ 62 ] = { + [ 1 ] = { ":authority", 10, 0 }, + [ 2 ] = { ":method" "GET", 7, 3 }, + [ 3 ] = { ":method" "POST", 7, 4 }, + [ 4 ] = { ":path" "/", 5, 1 }, + [ 5 ] = { ":path" "/index.html", 5, 11 }, + [ 6 ] = { ":scheme" "http", 7, 4 }, + [ 7 ] = { ":scheme" "https", 7, 5 }, + [ 8 ] = { ":status" "200", 7, 3 }, + [ 9 ] = { ":status" "204", 7, 3 }, + [ 10 ] = { ":status" "206", 7, 3 }, + [ 11 ] = { ":status" "304", 7, 3 }, + [ 12 ] = { ":status" "400", 7, 3 }, + [ 13 ] = { ":status" "404", 7, 3 }, + [ 14 ] = { ":status" "500", 7, 3 }, + [ 15 ] = { "accept-charset", 14, 0 }, + [ 16 ] = { "accept-encoding" "gzip, deflate", 15, 13 }, + [ 17 ] = { "accept-language", 15, 0 }, + [ 18 ] = { "accept-ranges", 13, 0 }, + [ 19 ] = { "accept", 6, 0 }, + [ 20 ] = { "access-control-allow-origin", 27, 0 }, + [ 21 ] = { "age", 3, 0 }, + [ 22 ] = { "allow", 5, 0 }, + [ 23 ] = { "authorization", 13, 0 }, + [ 24 ] = { "cache-control", 13, 0 }, + [ 25 ] = { "content-disposition", 19, 0 }, + [ 26 ] = { "content-encoding", 16, 0 }, + [ 27 ] = { "content-language", 16, 0 }, + [ 28 ] = { "content-length", 14, 0 }, + [ 29 ] = { "content-location", 16, 0 }, + [ 30 ] = { "content-range", 13, 0 }, + [ 31 ] = { "content-type", 12, 0 }, + [ 32 ] = { "cookie", 6, 0 }, + [ 33 ] = { "date", 4, 0 }, + [ 34 ] = { "etag", 4, 0 }, + [ 35 ] = { "expect", 6, 0 }, + [ 36 ] = { "expires", 7, 0 }, + [ 37 ] = { "from", 4, 0 }, + [ 38 ] = { "host", 4, 0 }, + [ 39 ] = { "if-match", 8, 0 }, + [ 40 ] = { "if-modified-since", 17, 0 }, + [ 41 ] = { "if-none-match", 13, 0 }, + [ 42 ] = { "if-range", 8, 0 }, + [ 43 ] = { "if-unmodified-since", 19, 0 }, + [ 44 ] = { "last-modified", 13, 0 }, + [ 45 ] = { "link", 4, 0 }, + [ 46 ] = { "location", 8, 0 }, + [ 47 ] = { "max-forwards", 12, 0 }, + [ 48 ] = { "proxy-authenticate", 18, 0 }, + [ 49 ] = { "proxy-authorization", 19, 0 }, + [ 50 ] = { "range", 5, 0 }, + [ 51 ] = { "referer", 7, 0 }, + [ 52 ] = { "refresh", 7, 0 }, + [ 53 ] = { "retry-after", 11, 0 }, + [ 54 ] = { "server", 6, 0 }, + [ 55 ] = { "set-cookie", 10, 0 }, + [ 56 ] = { "strict-transport-security", 25, 0 }, + [ 57 ] = { "transfer-encoding", 17, 0 }, + [ 58 ] = { "user-agent", 10, 0 }, + [ 59 ] = { "vary", 4, 0 }, + [ 60 ] = { "via", 3, 0 }, + [ 61 ] = { "www-authenticate", 16, 0 } +}; + +fd_hpack_rd_t * +fd_hpack_rd_init( fd_hpack_rd_t * rd, + uchar const * src, + ulong srcsz ) { + *rd = (fd_hpack_rd_t) { + .src = src, + .src_end = src+srcsz + }; + /* FIXME slow */ + /* Skip over Dynamic Table Size Updates */ + while( FD_LIKELY( rd->src < rd->src_end ) ) { + uint b0 = rd->src[0]; + if( FD_UNLIKELY( (b0&0xe0)==0x20 ) ) { + ulong max_sz = fd_hpack_rd_varint( rd, b0, 0x1f ); + if( FD_UNLIKELY( max_sz!=0UL ) ) break; /* FIXME hacky */ + rd->src++; + } else { + break; + } + } + return rd; +} + +/* fd_hpack_rd_indexed selects a header from HPACK dictionaries. + Currently, only supports the static table. (Pretends that the + dynamic table size is 0). */ + +static uint +fd_hpack_rd_indexed( fd_h2_hdr_t * hdr, + ulong idx ) { + if( FD_UNLIKELY( idx==0 || idx>61 ) ) return FD_H2_ERR_COMPRESSION; + fd_hpack_static_entry_t const * entry = &fd_hpack_static_table[ idx ]; + *hdr = (fd_h2_hdr_t) { + .name = entry->entry, + .name_len = entry->name_len, + .value = entry->entry + entry->name_len, + .value_len = entry->value_len, + .hint = (ushort)idx | FD_H2_HDR_HINT_NAME_INDEXED, + }; + return FD_H2_SUCCESS; +} + +static uint +fd_hpack_rd_next_raw( fd_hpack_rd_t * rd, + fd_h2_hdr_t * hdr ) { + uchar const * end = rd->src_end; + if( FD_UNLIKELY( rd->src >= end ) ) FD_LOG_CRIT(( "fd_hpack_rd_next called out of bounds" )); + + uint b0 = *(rd->src++); + + if( (b0&0xc0)==0x80 ) { + /* name indexed, value indexed, index in [0,63], varint sz 0 */ + uint err = fd_hpack_rd_indexed( hdr, b0&0x7f ); + hdr->hint |= FD_H2_HDR_HINT_VALUE_INDEXED; + return err; + } + + if( b0==0x40 || b0==0x00 || b0==0x10 ) { + /* name literal, value literal */ + if( FD_UNLIKELY( rd->src+2 > end ) ) return FD_H2_ERR_COMPRESSION; + + uint name_word = *(rd->src++); + ulong name_len = fd_hpack_rd_varint( rd, name_word, 0x7f ); + if( FD_UNLIKELY( name_len==ULONG_MAX ) ) return FD_H2_ERR_COMPRESSION; + if( FD_UNLIKELY( rd->src+name_len >= end ) ) return FD_H2_ERR_COMPRESSION; + uchar const * name_p = rd->src; + rd->src += name_len; + + uint value_word = *(rd->src++); + ulong value_len = fd_hpack_rd_varint( rd, value_word, 0x7f ); + if( FD_UNLIKELY( value_len==ULONG_MAX ) ) return FD_H2_ERR_COMPRESSION; + if( FD_UNLIKELY( rd->src+value_len > end ) ) return FD_H2_ERR_COMPRESSION; + uchar const * value_p = rd->src; + rd->src += value_len; + + hdr->name = (char const *)name_p; + hdr->name_len = (ushort)name_len; + hdr->value = (char const *)value_p; + hdr->value_len = (uint)value_len; + hdr->hint = fd_ushort_if( name_word&0x80, FD_H2_HDR_HINT_NAME_HUFFMAN, 0 ) | + fd_ushort_if( value_word&0x80, FD_H2_HDR_HINT_VALUE_HUFFMAN, 0 ); + return FD_H2_SUCCESS; + } + + if( (b0&0xc0)==0x40 || (b0&0xf0)==0x00 || (b0&0xf0)==0x10 ) { + /* name indexed, value literal */ + uint name_mask = (b0&0xc0)==0x40 ? 0x3f : 0x0f; + ulong name_idx = fd_hpack_rd_varint( rd, b0, name_mask ); + + uint value_word = *(rd->src++); + ulong value_len = fd_hpack_rd_varint( rd, value_word, 0x7f ); + if( FD_UNLIKELY( value_len==ULONG_MAX ) ) return FD_H2_ERR_COMPRESSION; + if( FD_UNLIKELY( rd->src+value_len > end ) ) return FD_H2_ERR_COMPRESSION; + uchar const * value_p = rd->src; + rd->src += value_len; + + uint err = fd_hpack_rd_indexed( hdr, name_idx ); + if( FD_UNLIKELY( err ) ) return FD_H2_ERR_COMPRESSION; + hdr->value = (char const *)value_p; + hdr->value_len = (uint)value_len; + hdr->hint |= fd_ushort_if( value_word&0x80, FD_H2_HDR_HINT_VALUE_HUFFMAN, 0 ); + return FD_H2_SUCCESS; + } + + if( FD_UNLIKELY( (b0&0xc0)==0xc0 ) ) { + /* name indexed, value indexed, index >=128 */ + ulong idx = fd_hpack_rd_varint( rd, b0, 0x7f ); /* may fail */ + return fd_hpack_rd_indexed( hdr, idx ); + } + + /* FIXME slow */ + /* Skip over Dynamic Table Size Updates */ + while( FD_LIKELY( rd->src < end ) ) { + b0 = rd->src[0]; + if( FD_UNLIKELY( (b0&0xe0)==0x20 ) ) { + ulong max_sz = fd_hpack_rd_varint( rd, b0, 0x1f ); + if( FD_UNLIKELY( max_sz!=0UL ) ) return FD_H2_ERR_COMPRESSION; + rd->src++; + } else { + break; + } + } + + /* Unknown HPACK instruction */ + return FD_H2_ERR_COMPRESSION; +} + +/* fd_hpack_decoded_sz_max returns an upper bound for the number of + decoded bytes given an arbitrary HPACK Huffman coding of enc_sz + bytes. The smallest HPACK symbol is 5 bits large. Therefore, the + true bound is closer to (enc_sz*8)/5. To defend against possible + bugs in huff_decode_table, we use a more conservative estimate, + namely the greatest amount of bytes that nghttp2_hd_huff_decode can + produce regardless of the content of huff_decode_table. */ + +static inline ulong +fd_hpack_decoded_sz_max( ulong enc_sz ) { + return enc_sz*2UL; +} + +uint +fd_hpack_rd_next( fd_hpack_rd_t * hpack_rd, + fd_h2_hdr_t * hdr, + uchar ** scratch, + uchar * scratch_end ) { + uint err = fd_hpack_rd_next_raw( hpack_rd, hdr ); + if( FD_UNLIKELY( err ) ) return err; + + uchar * scratch_ = *scratch; + + if( hdr->hint & FD_H2_HDR_HINT_NAME_HUFFMAN ) { + if( FD_UNLIKELY( scratch_+fd_hpack_decoded_sz_max( hdr->name_len )>scratch_end ) ) return FD_H2_ERR_COMPRESSION; + nghttp2_hd_huff_decode_context ctx[1]; + nghttp2_hd_huff_decode_context_init( ctx ); + nghttp2_buf buf = { .last = scratch_ }; + if( FD_UNLIKELY( nghttp2_hd_huff_decode( ctx, &buf, (uchar const *)hdr->name, hdr->name_len, 1 )<0 ) ) return FD_H2_ERR_COMPRESSION; + hdr->name = (char const *)scratch_; + hdr->name_len = (ushort)( buf.last-scratch_ ); + scratch_ = buf.last; + } + + if( hdr->hint & FD_H2_HDR_HINT_VALUE_HUFFMAN ) { + if( FD_UNLIKELY( scratch_+fd_hpack_decoded_sz_max( hdr->value_len )>scratch_end ) ) return FD_H2_ERR_COMPRESSION; + nghttp2_hd_huff_decode_context ctx[1]; + nghttp2_hd_huff_decode_context_init( ctx ); + nghttp2_buf buf = { .last = scratch_ }; + if( FD_UNLIKELY( nghttp2_hd_huff_decode( ctx, &buf, (uchar const *)hdr->value, hdr->value_len, 1 )<0 ) ) return FD_H2_ERR_COMPRESSION; + hdr->value = (char const *)scratch_; + hdr->value_len = (ushort)( buf.last-scratch_ ); + scratch_ = buf.last; + } + + *scratch = scratch_; + hdr->hint &= (ushort)~FD_H2_HDR_HINT_HUFFMAN; + return FD_H2_SUCCESS; +} diff --git a/src/waltz/h2/fd_hpack.h b/src/waltz/h2/fd_hpack.h new file mode 100644 index 0000000000..cd1ca4ccc5 --- /dev/null +++ b/src/waltz/h2/fd_hpack.h @@ -0,0 +1,97 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_hpack_h +#define HEADER_fd_src_waltz_h2_fd_hpack_h + +/* fd_hpack.h provides APIs for HPACK compression and decompression. + + Supports the static table and Huffman string coding. Does not use + the dynamic table while encoding. Assumes that the endpoint used + HTTP/2 SETTINGS to force the peer's dynamic table size to zero. */ + +#include "fd_h2_base.h" + +/* fd_h2_hdr_t points to an HTTP/2 header name:value pair. + + {name,value} point to decoded header values stored either in the + hardcoded HPACK static table, the binary frame, or a scratch buffer. + It is not guaranteed that these are valid ASCII. These are NOT + null-terminated. + + (hint&FD_H2_HDR_HINT_INDEXED) indicates that the HPACK coding of the + header referenced a static table entry. The index of the entry is + in the low 6 bits. + + (hint&FD_H2_HDR_HINT_HUFFMAN) is internal and can be safely ignored, + as fd_hpack_rd_next takes care of Huffman coding. */ + +struct fd_h2_hdr { + char const * name; + char const * value; + ushort name_len; + ushort hint; + uint value_len; +}; + +typedef struct fd_h2_hdr fd_h2_hdr_t; + +#define FD_H2_HDR_HINT_NAME_HUFFMAN ((ushort)0x8000) /* name is huffman coded */ +#define FD_H2_HDR_HINT_VALUE_HUFFMAN ((ushort)0x4000) /* value is huffman coded */ +#define FD_H2_HDR_HINT_HUFFMAN ((ushort)(FD_H2_HDR_HINT_NAME_HUFFMAN|FD_H2_HDR_HINT_VALUE_HUFFMAN)) +#define FD_H2_HDR_HINT_NAME_INDEXED ((ushort)0x2000) /* name was indexed from table */ +#define FD_H2_HDR_HINT_VALUE_INDEXED ((ushort)0x1000) /* value was indexed from table */ +#define FD_H2_HDR_HINT_INDEXED ((ushort)(FD_H2_HDR_HINT_NAME_INDEXED|FD_H2_HDR_HINT_VALUE_INDEXED)) +#define FD_H2_HDR_HINT_GET_INDEX(hint) ((uchar)((hint)&0xFF)) + +/* An fd_hpack_rd_t object reads a block of HPACK-encoded HTTP/2 + headers. For example usage, see test_hpack. */ + +struct fd_hpack_rd { + uchar const * src; + uchar const * src_end; +}; + +typedef struct fd_hpack_rd fd_hpack_rd_t; + +FD_PROTOTYPES_BEGIN + +/* fd_hpack_rd_init initializes a hpack_rd for reading of the header + block in src. hpack_rd has a read interest in src for its entire + lifetime. */ + +fd_hpack_rd_t * +fd_hpack_rd_init( fd_hpack_rd_t * rd, + uchar const * src, + ulong srcsz ); + +/* fd_hpack_rd_done returns 1 if all header entries were read from + hpack_rd. Returns 0 if fd_hpack_rd_next should be called again. */ + +static inline int +fd_hpack_rd_done( fd_hpack_rd_t const * rd ) { + return rd->src >= rd->src_end; +} + +/* fd_hpack_rd_next reads the next header from hpack_rd. hdr is + populated with pointers to the decoded data. These pointers either + point into hpack_rd->src or *scratch. + + *scratch is assumed to point to the next free byte in a scratch + buffer. scratch_end points one past the last byte of the scratch + buffer. + + Returns FD_H2_SUCCESS, populates header, and updates *scratch on + success. On failure, returns FD_H2_ERR_COMPRESSION and leaves + *scratch intact. Reasons for failure include HPACK parse error, + out-of-bounds table index, use of the dynamic table, Huffman coding + error, or out of scratch space. The caller should assume that *hdr + and **scratch (the free bytes in the scratch buffer, not the pointer + itself) are invalidated/filled with garbage on failure. */ + +uint +fd_hpack_rd_next( fd_hpack_rd_t * hpack_rd, + fd_h2_hdr_t * hdr, + uchar ** scratch, + uchar * scratch_end ); + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_waltz_h2_fd_hpack_h */ diff --git a/src/waltz/h2/fd_hpack_private.h b/src/waltz/h2/fd_hpack_private.h new file mode 100644 index 0000000000..a5705342c0 --- /dev/null +++ b/src/waltz/h2/fd_hpack_private.h @@ -0,0 +1,91 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_hpack_private_h +#define HEADER_fd_src_waltz_h2_fd_hpack_private_h + +#include "fd_hpack.h" + +#if FD_HAS_X86 +#include +#endif + +/* Simple HPACK static table. + FIXME could be made faster/smaller */ + +struct fd_hpack_static_entry { + char const * entry; + uchar name_len; + uchar value_len; +}; + +typedef struct fd_hpack_static_entry fd_hpack_static_entry_t; + +FD_PROTOTYPES_BEGIN + +extern fd_hpack_static_entry_t const +fd_hpack_static_table[ 62 ]; + +/* fd_hpack_rd_varint reads a varint with up to 8 bytes encoded length + (not including prefix). addend is (2^n)-1 where n is the varint + prefix bit count. prefix is the actual value of the varint prefix. + Returns a value in [0,2^56) on success. Returns ULONG_MAX on decode + failure. */ + +static inline ulong +fd_hpack_rd_varint( fd_hpack_rd_t * rd, + uint prefix, + uint addend ) { + prefix &= addend; + /* FIXME does not detect overflow */ + + /* Length is 0 */ + if( prefixsrc+8 <= rd->src_end ) ) { + /* happy path: speculatively read oob */ + enc = fd_ulong_load_8( rd->src ); + } else { + /* slow path: carefully memcpy, handle potentially corrupt src_end */ + ulong sz = fd_ulong_min( (ulong)rd->src_end - (ulong)rd->src, 8UL ); + if( FD_UNLIKELY( !sz ) ) return ULONG_MAX; /* eof */ + fd_memcpy( &enc, rd->src, sz ); + } + + /* sz_run is a bit pattern indicating: + + length 1 => least-significant one bit is at index 7 + length 2 => - " - 15 + ... + length n => - " - (8*n)-1 */ + ulong sz_run = ~( enc | 0x7f7f7f7f7f7f7f7fUL ); + if( FD_UNLIKELY( sz_run==0 ) ) return ULONG_MAX; /* unterminated varint */ + int sz_bits = fd_ulong_find_lsb( sz_run )+1; + ulong sz = (ulong)sz_bits>>3; + + /* Mask off garbage bits */ + enc &= fd_ulong_shift_left( 1UL, sz_bits )-1UL; + + /* Remove varint length bits */ +#if FD_HAS_X86 && defined(__BMI2__) + ulong result = _pext_u64( enc, 0x7f7f7f7f7f7f7f7fUL ); +#else + ulong result = + ( ( enc&0x000000000000007fUL )>>0 ) | + ( ( enc&0x0000000000007f00UL )>>1 ) | + ( ( enc&0x00000000007f0000UL )>>2 ) | + ( ( enc&0x000000007f000000UL )>>3 ) | + ( ( enc&0x0000007f00000000UL )>>4 ) | + ( ( enc&0x00007f0000000000UL )>>5 ) | + ( ( enc&0x007f000000000000UL )>>6 ) | + ( ( enc&0x7f00000000000000UL )>>7 ); +#endif + + uchar const * src_end = rd->src+sz; + if( FD_UNLIKELY( src_end>rd->src_end ) ) return ULONG_MAX; /* eof */ + rd->src = src_end; + return result+addend; +} + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_waltz_h2_fd_hpack_private_h */ diff --git a/src/waltz/h2/fd_hpack_wr.h b/src/waltz/h2/fd_hpack_wr.h new file mode 100644 index 0000000000..a0edc54d9f --- /dev/null +++ b/src/waltz/h2/fd_hpack_wr.h @@ -0,0 +1,173 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_hpack_wr_h +#define HEADER_fd_src_waltz_h2_fd_hpack_wr_h + +/* fd_hpack_wr.h provides simple APIs to generate HPACK header entries. + Generates somewhat wasteful serializations, but is quite simple. */ + +#include "fd_h2_base.h" +#include "fd_h2_rbuf.h" + +#define FD_HPACK_INDEXED_SHORT( val ) ((uchar)( 0x80|(val) )) + +#if FD_HAS_X86 +#include +#endif + +FD_PROTOTYPES_BEGIN + +static inline ulong +fd_hpack_wr_varint( + uchar code[9], + uint prefix, + uint addend, + ulong number /* in [0,2^56) */ +) { + ulong sz; + if( number> shift; + if( shift==64 ) mask = 0UL; + FD_STORE( ulong, code+1, enc|mask ); + sz = 2 + (ulong)( msb>>3 ); + } + return sz; +} + +static inline int +fd_hpack_wr_private_indexed( fd_h2_rbuf_t * rbuf_tx, + ulong idx ) { + if( FD_UNLIKELY( !fd_h2_rbuf_free_sz( rbuf_tx ) ) ) return 0; + uchar code[1] = { FD_HPACK_INDEXED_SHORT( idx ) }; + fd_h2_rbuf_push( rbuf_tx, code, 1 ); + return 1; +} + +static inline int +fd_hpack_wr_private_name_indexed_0( + fd_h2_rbuf_t * rbuf_tx, + ulong key, + ulong value_len /* in [0,128) */ +) { + uchar prefix[10] = { (uchar)key }; + if( FD_UNLIKELY( fd_h2_rbuf_free_sz( rbuf_tx ) < sizeof(prefix)+value_len ) ) return 0; + ulong prefix_len = 1+fd_hpack_wr_varint( prefix+1, 0x00, 0x7f, value_len ); + fd_h2_rbuf_push( rbuf_tx, prefix, prefix_len ); + return 1; +} + +static inline int +fd_hpack_wr_method_post( fd_h2_rbuf_t * rbuf_tx ) { + return fd_hpack_wr_private_indexed( rbuf_tx, 0x03 ); +} + +static inline int +fd_hpack_wr_scheme( fd_h2_rbuf_t * rbuf_tx, + int is_https ) { + if( is_https ) { + return fd_hpack_wr_private_indexed( rbuf_tx, 0x07 ); + } else { + return fd_hpack_wr_private_indexed( rbuf_tx, 0x06 ); + } +} + +/* fd_hpack_wr_path writes a ':path: ...' header. path_len must be in + [0,128). */ + +static inline int +fd_hpack_wr_path( fd_h2_rbuf_t * rbuf_tx, + char const * path, + ulong path_len ) { + if( FD_UNLIKELY( !fd_hpack_wr_private_name_indexed_0( rbuf_tx, 0x04, path_len ) ) ) return 0; + fd_h2_rbuf_push( rbuf_tx, path, path_len ); + return 1; +} + +/* fd_hpack_wr_trailers writes the 'te: trailers' header. */ + +static inline int +fd_hpack_wr_trailers( fd_h2_rbuf_t * rbuf_tx ) { + static char const code[] = + "\x00" + "\x02" "te" + "\x08" "trailers"; + if( FD_UNLIKELY( fd_h2_rbuf_free_sz( rbuf_tx ) +#include + +#include "fd_h2.h" +#include "../../util/fd_util.h" + +struct fuzz_h2_ctx { + fd_h2_rbuf_t rbuf_tx[1]; + fd_h2_conn_t conn[1]; + fd_h2_stream_t stream[1]; + fd_h2_tx_op_t tx_op[1]; +}; + +typedef struct fuzz_h2_ctx fuzz_h2_ctx_t; + +static FD_TL fuzz_h2_ctx_t g_ctx; + +static FD_TL fd_rng_t g_rng[1]; + +/* Stream leak detector (detects unbalanced callbacks) */ +static FD_TL long g_stream_cnt; + +/* Double callback detector */ +static FD_TL long g_conn_final_cnt; + +static void +test_response_continue( void ) { + if( !g_ctx.stream->stream_id ) return; + fd_h2_tx_op_copy( g_ctx.conn, g_ctx.stream, g_ctx.rbuf_tx, g_ctx.tx_op ); + if( g_ctx.stream->state==FD_H2_STREAM_STATE_CLOSED ) { + g_stream_cnt--; + memset( g_ctx.tx_op, 0, sizeof(g_ctx.tx_op ) ); + memset( g_ctx.stream, 0, sizeof(g_ctx.stream) ); + } +} + +static void +test_response_init( fd_h2_conn_t * conn, + fd_h2_stream_t * stream ) { + (void)conn; + uint stream_id = stream->stream_id; + + fd_h2_rbuf_t * rbuf_tx = g_ctx.rbuf_tx; + uchar hpack[] = { 0x88 /* :status: 200 */ }; + fd_h2_tx( rbuf_tx, hpack, sizeof(hpack), FD_H2_FRAME_TYPE_HEADERS, FD_H2_FLAG_END_HEADERS, stream_id ); + + fd_h2_tx_op_t * tx_op = g_ctx.tx_op; + fd_h2_tx_op_init( tx_op, "Ok", 2UL, FD_H2_FLAG_END_STREAM ); + test_response_continue(); +} + +static fd_h2_stream_t * +cb_stream_create( fd_h2_conn_t * conn, + uint stream_id ) { + (void)conn; (void)stream_id; + if( g_ctx.stream->stream_id ) { + return NULL; + } + fd_h2_stream_init( g_ctx.stream ); + g_stream_cnt++; + return g_ctx.stream; +} + +static fd_h2_stream_t * +cb_stream_query( fd_h2_conn_t * conn, + uint stream_id ) { + assert( conn == g_ctx.conn ); + if( g_ctx.stream->stream_id!=stream_id ) return NULL; + return g_ctx.stream; +} + +static void +cb_conn_established( fd_h2_conn_t * conn ) { + assert( conn == g_ctx.conn ); + return; +} + +static void +cb_conn_final( fd_h2_conn_t * conn, + uint h2_err, + int closed_by ) { + assert( conn == g_ctx.conn ); + assert( closed_by==0 || closed_by==1 ); + (void)h2_err; + g_stream_cnt = 0L; + g_conn_final_cnt++; + return; +} + +static void +cb_headers( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + void const * data, + ulong data_sz, + ulong flags ) { + fd_hpack_rd_t hpack_rd[1]; + fd_hpack_rd_init( hpack_rd, data, data_sz ); + while( !fd_hpack_rd_done( hpack_rd ) ) { + static FD_TL uchar scratch_buf[ 4096 ]; + uchar * scratch = scratch_buf; + fd_h2_hdr_t hdr[1]; + uint err = fd_hpack_rd_next( hpack_rd, hdr, &scratch, scratch_buf+sizeof(scratch_buf) ); + if( FD_UNLIKELY( err ) ) { + fd_h2_conn_error( conn, err ); + return; + } + } + if( flags & FD_H2_FLAG_END_STREAM ) { + test_response_init( conn, stream ); + } + return; +} + +static void +cb_data( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + void const * data, + ulong data_sz, + ulong flags ) { + assert( conn == g_ctx.conn ); + (void)stream; (void)data; (void)data_sz; (void)flags; + if( flags & FD_H2_FLAG_END_STREAM ) { + test_response_init( conn, stream ); + } + return; +} + +static void +cb_rst_stream( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + uint error_code, + int closed_by ) { + (void)stream; (void)error_code; + assert( conn == g_ctx.conn ); + assert( closed_by==0 || closed_by==1 ); + memset( &g_ctx.stream, 0, sizeof(fd_h2_stream_t) ); + g_stream_cnt--; + return; +} + +static void +cb_window_update( fd_h2_conn_t * conn, + uint increment ) { + (void)conn; (void)increment; + return; +} + +static void +cb_stream_window_update( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + uint increment ) { + (void)conn; (void)stream; (void)increment; + return; +} + +static fd_h2_callbacks_t fuzz_h2_cb = { + .stream_create = cb_stream_create, + .stream_query = cb_stream_query, + .conn_established = cb_conn_established, + .conn_final = cb_conn_final, + .headers = cb_headers, + .data = cb_data, + .rst_stream = cb_rst_stream, + .window_update = cb_window_update, + .stream_window_update = cb_stream_window_update +}; + +int +LLVMFuzzerInitialize( int * argc, + char *** argv ) { + /* Set up shell without signal handlers */ + putenv( "FD_LOG_BACKTRACE=0" ); + fd_boot( argc, argv ); + (void)atexit( fd_halt ); + fd_log_level_core_set(1); /* crash on info log */ + return 0; +} + +int +LLVMFuzzerTestOneInput( uchar const * data, + ulong size ) { + memset( &g_ctx, 0, sizeof(fuzz_h2_ctx_t) ); + + if( size<4 ) return -1; + uint seed = FD_LOAD( uint, data ); + data += 4; size -= 4; + + fd_rng_t * rng = fd_rng_join( fd_rng_new( g_rng, seed, 0UL ) ); + + uchar buf_rx [ 256 ]; + uchar buf_tx [ 256 ]; + uchar scratch[ 256 ]; + fd_h2_rbuf_t rbuf_rx[1]; + fd_h2_rbuf_t * rbuf_tx = g_ctx.rbuf_tx; + fd_h2_rbuf_init( rbuf_rx, buf_rx, sizeof(buf_rx) ); + fd_h2_rbuf_init( rbuf_tx, buf_tx, sizeof(buf_tx) ); + + if( seed&1 ) { + fd_h2_conn_init_client( g_ctx.conn ); + } else { + fd_h2_conn_init_server( g_ctx.conn ); + } + g_ctx.conn->self_settings.max_frame_size = 256; + + g_stream_cnt = 0L; + g_conn_final_cnt = 0L; + + while( size ) { + fd_h2_tx_control( g_ctx.conn, rbuf_tx, &fuzz_h2_cb ); + rbuf_tx->lo_off = rbuf_tx->hi_off; + rbuf_tx->lo = rbuf_tx->hi; + + if( FD_UNLIKELY( g_ctx.conn->flags & FD_H2_CONN_FLAGS_DEAD ) ) { + assert( g_conn_final_cnt==1 ); + assert( g_stream_cnt==0 ); + break; + } + + ulong chunk = fd_ulong_min( size, (fd_rng_uint( rng )&15)+1 ); + if( fd_h2_rbuf_used_sz( rbuf_rx )+chunk > rbuf_rx->bufsz ) break; + fd_h2_rbuf_push( rbuf_rx, data, chunk ); + data += chunk; size -= chunk; + + fd_h2_rx( g_ctx.conn, rbuf_rx, rbuf_tx, scratch, sizeof(scratch), &fuzz_h2_cb ); + } + fd_h2_tx_control( g_ctx.conn, rbuf_tx, &fuzz_h2_cb ); + + assert( g_stream_cnt>=0 ); + assert( g_conn_final_cnt==0 || g_conn_final_cnt==1 ); + if( g_stream_cnt==1 ) { + assert( g_ctx.stream->state==FD_H2_STREAM_STATE_OPEN || + g_ctx.stream->state==FD_H2_STREAM_STATE_CLOSING_RX || + g_ctx.stream->state==FD_H2_STREAM_STATE_CLOSING_TX ); + } + + fd_rng_delete( fd_rng_leave( rng ) ); + return 0; +} diff --git a/src/waltz/h2/fuzz_hpack_rd.c b/src/waltz/h2/fuzz_hpack_rd.c new file mode 100644 index 0000000000..13aa7689bd --- /dev/null +++ b/src/waltz/h2/fuzz_hpack_rd.c @@ -0,0 +1,39 @@ +#if !FD_HAS_HOSTED +#error "This target requires FD_HAS_HOSTED" +#endif + +#include +#include + +#include "fd_hpack.h" +#include "../../util/fd_util.h" + +int +LLVMFuzzerInitialize( int * argc, + char *** argv ) { + /* Set up shell without signal handlers */ + putenv( "FD_LOG_BACKTRACE=0" ); + fd_boot( argc, argv ); + atexit( fd_halt ); + fd_log_level_core_set(1); /* crash on info log */ + return 0; +} + +int +LLVMFuzzerTestOneInput( uchar const * data, + ulong size ) { + fd_hpack_rd_t rd[1]; + fd_hpack_rd_init( rd, data, size ); + uchar const * prev = data; + while( !fd_hpack_rd_done( rd ) ) { + fd_h2_hdr_t hdr[1]; + uchar buf[ 128 ]; + uchar * bufp = buf; + if( FD_UNLIKELY( fd_hpack_rd_next( rd, hdr, &bufp, buf+sizeof(buf) )!=FD_H2_SUCCESS ) ) break; + /* FIXME validate content of hdr */ + assert( rd->src > prev ); /* must advance */ + assert( bufp>=buf && bufp<=buf+sizeof(buf) ); + prev = rd->src; + } + return 0; +} diff --git a/src/waltz/h2/nghttp2_hd_huffman.c b/src/waltz/h2/nghttp2_hd_huffman.c new file mode 100644 index 0000000000..c680671aa1 --- /dev/null +++ b/src/waltz/h2/nghttp2_hd_huffman.c @@ -0,0 +1,70 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "nghttp2_hd_huffman.h" + +#include +#include +#include + +//#include "nghttp2_hd.h" +//#include "nghttp2_net.h" + +void nghttp2_hd_huff_decode_context_init(nghttp2_hd_huff_decode_context *ctx) { + ctx->fstate = NGHTTP2_HUFF_ACCEPTED; +} + +nghttp2_ssize nghttp2_hd_huff_decode(nghttp2_hd_huff_decode_context *ctx, + nghttp2_buf *buf, const uint8_t *src, + size_t srclen, int final) { + const uint8_t *end = src + srclen; + nghttp2_huff_decode node = {ctx->fstate, 0}; + const nghttp2_huff_decode *t = &node; + uint8_t c; + + /* We use the decoding algorithm described in + - http://graphics.ics.uci.edu/pub/Prefix.pdf [!!! NO LONGER VALID !!!] + - https://ics.uci.edu/~dan/pubs/Prefix.pdf + - https://github.com/nghttp2/nghttp2/files/15141264/Prefix.pdf */ + for (; src != end;) { + c = *src++; + t = &huff_decode_table[t->fstate & 0x1ff][c >> 4]; + if (t->fstate & NGHTTP2_HUFF_SYM) { + *buf->last++ = t->sym; + } + + t = &huff_decode_table[t->fstate & 0x1ff][c & 0xf]; + if (t->fstate & NGHTTP2_HUFF_SYM) { + *buf->last++ = t->sym; + } + } + + ctx->fstate = t->fstate; + + if (final && !(ctx->fstate & NGHTTP2_HUFF_ACCEPTED)) { + return NGHTTP2_ERR_HEADER_COMP; + } + + return (nghttp2_ssize)srclen; +} diff --git a/src/waltz/h2/nghttp2_hd_huffman.h b/src/waltz/h2/nghttp2_hd_huffman.h new file mode 100644 index 0000000000..555bd619f3 --- /dev/null +++ b/src/waltz/h2/nghttp2_hd_huffman.h @@ -0,0 +1,125 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NGHTTP2_HD_HUFFMAN_H +#define NGHTTP2_HD_HUFFMAN_H + +//#ifdef HAVE_CONFIG_H +//# include +//#endif /* HAVE_CONFIG_H */ + +//#include +#include +#include + +typedef enum { + /* FSA accepts this state as the end of huffman encoding + sequence. */ + NGHTTP2_HUFF_ACCEPTED = 1 << 14, + /* This state emits symbol */ + NGHTTP2_HUFF_SYM = 1 << 15, +} nghttp2_huff_decode_flag; + +typedef struct { + /* fstate is the current huffman decoding state, which is actually + the node ID of internal huffman tree with + nghttp2_huff_decode_flag OR-ed. We have 257 leaf nodes, but they + are identical to root node other than emitting a symbol, so we + have 256 internal nodes [1..255], inclusive. The node ID 256 is + a special node and it is a terminal state that means decoding + failed. */ + uint16_t fstate; + /* symbol if NGHTTP2_HUFF_SYM flag set */ + uint8_t sym; +} nghttp2_huff_decode; + +typedef nghttp2_huff_decode huff_decode_table_type[16]; + +typedef struct { + /* fstate is the current huffman decoding state. */ + uint16_t fstate; +} nghttp2_hd_huff_decode_context; + +typedef struct { + /* The number of bits in this code */ + uint32_t nbits; + /* Huffman code aligned to LSB */ + uint32_t code; +} nghttp2_huff_sym; + +extern const nghttp2_huff_sym huff_sym_table[]; +extern const nghttp2_huff_decode huff_decode_table[][16]; + +void nghttp2_hd_huff_decode_context_init(nghttp2_hd_huff_decode_context *ctx); + +typedef struct { + ///* This points to the beginning of the buffer. The effective range + // of buffer is [begin, end). */ + //uint8_t *begin; + ///* This points to the memory one byte beyond the end of the + // buffer. */ + //uint8_t *end; + ///* The position indicator for effective start of the buffer. pos <= + // last must be hold. */ + //uint8_t *pos; + /* The position indicator for effective one beyond of the end of the + buffer. last <= end must be hold. */ + uint8_t *last; + ///* Mark arbitrary position in buffer [begin, end) */ + //uint8_t *mark; +} nghttp2_buf; + +typedef long nghttp2_ssize; + +typedef enum { + /** + * Header block inflate/deflate error. + */ + NGHTTP2_ERR_HEADER_COMP = -523, +} nghttp2_error; + +/* + * Decodes the given data |src| with length |srclen|. The |ctx| must + * be initialized by nghttp2_hd_huff_decode_context_init(). The result + * will be written to |buf|. This function assumes that |buf| has the + * enough room to store the decoded byte string. + * + * The caller must set the |fin| to nonzero if the given input is the + * final block. + * + * This function returns the number of read bytes from the |in|. + * + * If this function fails, it returns one of the following negative + * return codes: + * + * NGHTTP2_ERR_NOMEM + * Out of memory. + * NGHTTP2_ERR_HEADER_COMP + * Decoding process has failed. + */ +nghttp2_ssize nghttp2_hd_huff_decode(nghttp2_hd_huff_decode_context *ctx, + nghttp2_buf *buf, const uint8_t *src, + size_t srclen, int fin); + +#endif /* NGHTTP2_HD_HUFFMAN_H */ diff --git a/src/waltz/h2/nghttp2_hd_huffman_data.c b/src/waltz/h2/nghttp2_hd_huffman_data.c new file mode 100644 index 0000000000..c8f4a6fa26 --- /dev/null +++ b/src/waltz/h2/nghttp2_hd_huffman_data.c @@ -0,0 +1,4980 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "nghttp2_hd_huffman.h" + +/* Generated by mkhufftbl.py */ + +const nghttp2_huff_sym huff_sym_table[] = { + {13, 0xffc00000u}, {23, 0xffffb000u}, {28, 0xfffffe20u}, {28, 0xfffffe30u}, + {28, 0xfffffe40u}, {28, 0xfffffe50u}, {28, 0xfffffe60u}, {28, 0xfffffe70u}, + {28, 0xfffffe80u}, {24, 0xffffea00u}, {30, 0xfffffff0u}, {28, 0xfffffe90u}, + {28, 0xfffffea0u}, {30, 0xfffffff4u}, {28, 0xfffffeb0u}, {28, 0xfffffec0u}, + {28, 0xfffffed0u}, {28, 0xfffffee0u}, {28, 0xfffffef0u}, {28, 0xffffff00u}, + {28, 0xffffff10u}, {28, 0xffffff20u}, {30, 0xfffffff8u}, {28, 0xffffff30u}, + {28, 0xffffff40u}, {28, 0xffffff50u}, {28, 0xffffff60u}, {28, 0xffffff70u}, + {28, 0xffffff80u}, {28, 0xffffff90u}, {28, 0xffffffa0u}, {28, 0xffffffb0u}, + {6, 0x50000000u}, {10, 0xfe000000u}, {10, 0xfe400000u}, {12, 0xffa00000u}, + {13, 0xffc80000u}, {6, 0x54000000u}, {8, 0xf8000000u}, {11, 0xff400000u}, + {10, 0xfe800000u}, {10, 0xfec00000u}, {8, 0xf9000000u}, {11, 0xff600000u}, + {8, 0xfa000000u}, {6, 0x58000000u}, {6, 0x5c000000u}, {6, 0x60000000u}, + {5, 0x0u}, {5, 0x8000000u}, {5, 0x10000000u}, {6, 0x64000000u}, + {6, 0x68000000u}, {6, 0x6c000000u}, {6, 0x70000000u}, {6, 0x74000000u}, + {6, 0x78000000u}, {6, 0x7c000000u}, {7, 0xb8000000u}, {8, 0xfb000000u}, + {15, 0xfff80000u}, {6, 0x80000000u}, {12, 0xffb00000u}, {10, 0xff000000u}, + {13, 0xffd00000u}, {6, 0x84000000u}, {7, 0xba000000u}, {7, 0xbc000000u}, + {7, 0xbe000000u}, {7, 0xc0000000u}, {7, 0xc2000000u}, {7, 0xc4000000u}, + {7, 0xc6000000u}, {7, 0xc8000000u}, {7, 0xca000000u}, {7, 0xcc000000u}, + {7, 0xce000000u}, {7, 0xd0000000u}, {7, 0xd2000000u}, {7, 0xd4000000u}, + {7, 0xd6000000u}, {7, 0xd8000000u}, {7, 0xda000000u}, {7, 0xdc000000u}, + {7, 0xde000000u}, {7, 0xe0000000u}, {7, 0xe2000000u}, {7, 0xe4000000u}, + {8, 0xfc000000u}, {7, 0xe6000000u}, {8, 0xfd000000u}, {13, 0xffd80000u}, + {19, 0xfffe0000u}, {13, 0xffe00000u}, {14, 0xfff00000u}, {6, 0x88000000u}, + {15, 0xfffa0000u}, {5, 0x18000000u}, {6, 0x8c000000u}, {5, 0x20000000u}, + {6, 0x90000000u}, {5, 0x28000000u}, {6, 0x94000000u}, {6, 0x98000000u}, + {6, 0x9c000000u}, {5, 0x30000000u}, {7, 0xe8000000u}, {7, 0xea000000u}, + {6, 0xa0000000u}, {6, 0xa4000000u}, {6, 0xa8000000u}, {5, 0x38000000u}, + {6, 0xac000000u}, {7, 0xec000000u}, {6, 0xb0000000u}, {5, 0x40000000u}, + {5, 0x48000000u}, {6, 0xb4000000u}, {7, 0xee000000u}, {7, 0xf0000000u}, + {7, 0xf2000000u}, {7, 0xf4000000u}, {7, 0xf6000000u}, {15, 0xfffc0000u}, + {11, 0xff800000u}, {14, 0xfff40000u}, {13, 0xffe80000u}, {28, 0xffffffc0u}, + {20, 0xfffe6000u}, {22, 0xffff4800u}, {20, 0xfffe7000u}, {20, 0xfffe8000u}, + {22, 0xffff4c00u}, {22, 0xffff5000u}, {22, 0xffff5400u}, {23, 0xffffb200u}, + {22, 0xffff5800u}, {23, 0xffffb400u}, {23, 0xffffb600u}, {23, 0xffffb800u}, + {23, 0xffffba00u}, {23, 0xffffbc00u}, {24, 0xffffeb00u}, {23, 0xffffbe00u}, + {24, 0xffffec00u}, {24, 0xffffed00u}, {22, 0xffff5c00u}, {23, 0xffffc000u}, + {24, 0xffffee00u}, {23, 0xffffc200u}, {23, 0xffffc400u}, {23, 0xffffc600u}, + {23, 0xffffc800u}, {21, 0xfffee000u}, {22, 0xffff6000u}, {23, 0xffffca00u}, + {22, 0xffff6400u}, {23, 0xffffcc00u}, {23, 0xffffce00u}, {24, 0xffffef00u}, + {22, 0xffff6800u}, {21, 0xfffee800u}, {20, 0xfffe9000u}, {22, 0xffff6c00u}, + {22, 0xffff7000u}, {23, 0xffffd000u}, {23, 0xffffd200u}, {21, 0xfffef000u}, + {23, 0xffffd400u}, {22, 0xffff7400u}, {22, 0xffff7800u}, {24, 0xfffff000u}, + {21, 0xfffef800u}, {22, 0xffff7c00u}, {23, 0xffffd600u}, {23, 0xffffd800u}, + {21, 0xffff0000u}, {21, 0xffff0800u}, {22, 0xffff8000u}, {21, 0xffff1000u}, + {23, 0xffffda00u}, {22, 0xffff8400u}, {23, 0xffffdc00u}, {23, 0xffffde00u}, + {20, 0xfffea000u}, {22, 0xffff8800u}, {22, 0xffff8c00u}, {22, 0xffff9000u}, + {23, 0xffffe000u}, {22, 0xffff9400u}, {22, 0xffff9800u}, {23, 0xffffe200u}, + {26, 0xfffff800u}, {26, 0xfffff840u}, {20, 0xfffeb000u}, {19, 0xfffe2000u}, + {22, 0xffff9c00u}, {23, 0xffffe400u}, {22, 0xffffa000u}, {25, 0xfffff600u}, + {26, 0xfffff880u}, {26, 0xfffff8c0u}, {26, 0xfffff900u}, {27, 0xfffffbc0u}, + {27, 0xfffffbe0u}, {26, 0xfffff940u}, {24, 0xfffff100u}, {25, 0xfffff680u}, + {19, 0xfffe4000u}, {21, 0xffff1800u}, {26, 0xfffff980u}, {27, 0xfffffc00u}, + {27, 0xfffffc20u}, {26, 0xfffff9c0u}, {27, 0xfffffc40u}, {24, 0xfffff200u}, + {21, 0xffff2000u}, {21, 0xffff2800u}, {26, 0xfffffa00u}, {26, 0xfffffa40u}, + {28, 0xffffffd0u}, {27, 0xfffffc60u}, {27, 0xfffffc80u}, {27, 0xfffffca0u}, + {20, 0xfffec000u}, {24, 0xfffff300u}, {20, 0xfffed000u}, {21, 0xffff3000u}, + {22, 0xffffa400u}, {21, 0xffff3800u}, {21, 0xffff4000u}, {23, 0xffffe600u}, + {22, 0xffffa800u}, {22, 0xffffac00u}, {25, 0xfffff700u}, {25, 0xfffff780u}, + {24, 0xfffff400u}, {24, 0xfffff500u}, {26, 0xfffffa80u}, {23, 0xffffe800u}, + {26, 0xfffffac0u}, {27, 0xfffffcc0u}, {26, 0xfffffb00u}, {26, 0xfffffb40u}, + {27, 0xfffffce0u}, {27, 0xfffffd00u}, {27, 0xfffffd20u}, {27, 0xfffffd40u}, + {27, 0xfffffd60u}, {28, 0xffffffe0u}, {27, 0xfffffd80u}, {27, 0xfffffda0u}, + {27, 0xfffffdc0u}, {27, 0xfffffde0u}, {27, 0xfffffe00u}, {26, 0xfffffb80u}, + {30, 0xfffffffcu}}; + +const nghttp2_huff_decode huff_decode_table[][16] = { + /* 0 */ + { + {0x04, 0}, + {0x05, 0}, + {0x07, 0}, + {0x08, 0}, + {0x0b, 0}, + {0x0c, 0}, + {0x10, 0}, + {0x13, 0}, + {0x19, 0}, + {0x1c, 0}, + {0x20, 0}, + {0x23, 0}, + {0x2a, 0}, + {0x31, 0}, + {0x39, 0}, + {0x4040, 0}, + }, + /* 1 */ + { + {0xc000, 48}, + {0xc000, 49}, + {0xc000, 50}, + {0xc000, 97}, + {0xc000, 99}, + {0xc000, 101}, + {0xc000, 105}, + {0xc000, 111}, + {0xc000, 115}, + {0xc000, 116}, + {0x0d, 0}, + {0x0e, 0}, + {0x11, 0}, + {0x12, 0}, + {0x14, 0}, + {0x15, 0}, + }, + /* 2 */ + { + {0x8001, 48}, + {0xc016, 48}, + {0x8001, 49}, + {0xc016, 49}, + {0x8001, 50}, + {0xc016, 50}, + {0x8001, 97}, + {0xc016, 97}, + {0x8001, 99}, + {0xc016, 99}, + {0x8001, 101}, + {0xc016, 101}, + {0x8001, 105}, + {0xc016, 105}, + {0x8001, 111}, + {0xc016, 111}, + }, + /* 3 */ + { + {0x8002, 48}, + {0x8009, 48}, + {0x8017, 48}, + {0xc028, 48}, + {0x8002, 49}, + {0x8009, 49}, + {0x8017, 49}, + {0xc028, 49}, + {0x8002, 50}, + {0x8009, 50}, + {0x8017, 50}, + {0xc028, 50}, + {0x8002, 97}, + {0x8009, 97}, + {0x8017, 97}, + {0xc028, 97}, + }, + /* 4 */ + { + {0x8003, 48}, + {0x8006, 48}, + {0x800a, 48}, + {0x800f, 48}, + {0x8018, 48}, + {0x801f, 48}, + {0x8029, 48}, + {0xc038, 48}, + {0x8003, 49}, + {0x8006, 49}, + {0x800a, 49}, + {0x800f, 49}, + {0x8018, 49}, + {0x801f, 49}, + {0x8029, 49}, + {0xc038, 49}, + }, + /* 5 */ + { + {0x8003, 50}, + {0x8006, 50}, + {0x800a, 50}, + {0x800f, 50}, + {0x8018, 50}, + {0x801f, 50}, + {0x8029, 50}, + {0xc038, 50}, + {0x8003, 97}, + {0x8006, 97}, + {0x800a, 97}, + {0x800f, 97}, + {0x8018, 97}, + {0x801f, 97}, + {0x8029, 97}, + {0xc038, 97}, + }, + /* 6 */ + { + {0x8002, 99}, + {0x8009, 99}, + {0x8017, 99}, + {0xc028, 99}, + {0x8002, 101}, + {0x8009, 101}, + {0x8017, 101}, + {0xc028, 101}, + {0x8002, 105}, + {0x8009, 105}, + {0x8017, 105}, + {0xc028, 105}, + {0x8002, 111}, + {0x8009, 111}, + {0x8017, 111}, + {0xc028, 111}, + }, + /* 7 */ + { + {0x8003, 99}, + {0x8006, 99}, + {0x800a, 99}, + {0x800f, 99}, + {0x8018, 99}, + {0x801f, 99}, + {0x8029, 99}, + {0xc038, 99}, + {0x8003, 101}, + {0x8006, 101}, + {0x800a, 101}, + {0x800f, 101}, + {0x8018, 101}, + {0x801f, 101}, + {0x8029, 101}, + {0xc038, 101}, + }, + /* 8 */ + { + {0x8003, 105}, + {0x8006, 105}, + {0x800a, 105}, + {0x800f, 105}, + {0x8018, 105}, + {0x801f, 105}, + {0x8029, 105}, + {0xc038, 105}, + {0x8003, 111}, + {0x8006, 111}, + {0x800a, 111}, + {0x800f, 111}, + {0x8018, 111}, + {0x801f, 111}, + {0x8029, 111}, + {0xc038, 111}, + }, + /* 9 */ + { + {0x8001, 115}, + {0xc016, 115}, + {0x8001, 116}, + {0xc016, 116}, + {0xc000, 32}, + {0xc000, 37}, + {0xc000, 45}, + {0xc000, 46}, + {0xc000, 47}, + {0xc000, 51}, + {0xc000, 52}, + {0xc000, 53}, + {0xc000, 54}, + {0xc000, 55}, + {0xc000, 56}, + {0xc000, 57}, + }, + /* 10 */ + { + {0x8002, 115}, + {0x8009, 115}, + {0x8017, 115}, + {0xc028, 115}, + {0x8002, 116}, + {0x8009, 116}, + {0x8017, 116}, + {0xc028, 116}, + {0x8001, 32}, + {0xc016, 32}, + {0x8001, 37}, + {0xc016, 37}, + {0x8001, 45}, + {0xc016, 45}, + {0x8001, 46}, + {0xc016, 46}, + }, + /* 11 */ + { + {0x8003, 115}, + {0x8006, 115}, + {0x800a, 115}, + {0x800f, 115}, + {0x8018, 115}, + {0x801f, 115}, + {0x8029, 115}, + {0xc038, 115}, + {0x8003, 116}, + {0x8006, 116}, + {0x800a, 116}, + {0x800f, 116}, + {0x8018, 116}, + {0x801f, 116}, + {0x8029, 116}, + {0xc038, 116}, + }, + /* 12 */ + { + {0x8002, 32}, + {0x8009, 32}, + {0x8017, 32}, + {0xc028, 32}, + {0x8002, 37}, + {0x8009, 37}, + {0x8017, 37}, + {0xc028, 37}, + {0x8002, 45}, + {0x8009, 45}, + {0x8017, 45}, + {0xc028, 45}, + {0x8002, 46}, + {0x8009, 46}, + {0x8017, 46}, + {0xc028, 46}, + }, + /* 13 */ + { + {0x8003, 32}, + {0x8006, 32}, + {0x800a, 32}, + {0x800f, 32}, + {0x8018, 32}, + {0x801f, 32}, + {0x8029, 32}, + {0xc038, 32}, + {0x8003, 37}, + {0x8006, 37}, + {0x800a, 37}, + {0x800f, 37}, + {0x8018, 37}, + {0x801f, 37}, + {0x8029, 37}, + {0xc038, 37}, + }, + /* 14 */ + { + {0x8003, 45}, + {0x8006, 45}, + {0x800a, 45}, + {0x800f, 45}, + {0x8018, 45}, + {0x801f, 45}, + {0x8029, 45}, + {0xc038, 45}, + {0x8003, 46}, + {0x8006, 46}, + {0x800a, 46}, + {0x800f, 46}, + {0x8018, 46}, + {0x801f, 46}, + {0x8029, 46}, + {0xc038, 46}, + }, + /* 15 */ + { + {0x8001, 47}, + {0xc016, 47}, + {0x8001, 51}, + {0xc016, 51}, + {0x8001, 52}, + {0xc016, 52}, + {0x8001, 53}, + {0xc016, 53}, + {0x8001, 54}, + {0xc016, 54}, + {0x8001, 55}, + {0xc016, 55}, + {0x8001, 56}, + {0xc016, 56}, + {0x8001, 57}, + {0xc016, 57}, + }, + /* 16 */ + { + {0x8002, 47}, + {0x8009, 47}, + {0x8017, 47}, + {0xc028, 47}, + {0x8002, 51}, + {0x8009, 51}, + {0x8017, 51}, + {0xc028, 51}, + {0x8002, 52}, + {0x8009, 52}, + {0x8017, 52}, + {0xc028, 52}, + {0x8002, 53}, + {0x8009, 53}, + {0x8017, 53}, + {0xc028, 53}, + }, + /* 17 */ + { + {0x8003, 47}, + {0x8006, 47}, + {0x800a, 47}, + {0x800f, 47}, + {0x8018, 47}, + {0x801f, 47}, + {0x8029, 47}, + {0xc038, 47}, + {0x8003, 51}, + {0x8006, 51}, + {0x800a, 51}, + {0x800f, 51}, + {0x8018, 51}, + {0x801f, 51}, + {0x8029, 51}, + {0xc038, 51}, + }, + /* 18 */ + { + {0x8003, 52}, + {0x8006, 52}, + {0x800a, 52}, + {0x800f, 52}, + {0x8018, 52}, + {0x801f, 52}, + {0x8029, 52}, + {0xc038, 52}, + {0x8003, 53}, + {0x8006, 53}, + {0x800a, 53}, + {0x800f, 53}, + {0x8018, 53}, + {0x801f, 53}, + {0x8029, 53}, + {0xc038, 53}, + }, + /* 19 */ + { + {0x8002, 54}, + {0x8009, 54}, + {0x8017, 54}, + {0xc028, 54}, + {0x8002, 55}, + {0x8009, 55}, + {0x8017, 55}, + {0xc028, 55}, + {0x8002, 56}, + {0x8009, 56}, + {0x8017, 56}, + {0xc028, 56}, + {0x8002, 57}, + {0x8009, 57}, + {0x8017, 57}, + {0xc028, 57}, + }, + /* 20 */ + { + {0x8003, 54}, + {0x8006, 54}, + {0x800a, 54}, + {0x800f, 54}, + {0x8018, 54}, + {0x801f, 54}, + {0x8029, 54}, + {0xc038, 54}, + {0x8003, 55}, + {0x8006, 55}, + {0x800a, 55}, + {0x800f, 55}, + {0x8018, 55}, + {0x801f, 55}, + {0x8029, 55}, + {0xc038, 55}, + }, + /* 21 */ + { + {0x8003, 56}, + {0x8006, 56}, + {0x800a, 56}, + {0x800f, 56}, + {0x8018, 56}, + {0x801f, 56}, + {0x8029, 56}, + {0xc038, 56}, + {0x8003, 57}, + {0x8006, 57}, + {0x800a, 57}, + {0x800f, 57}, + {0x8018, 57}, + {0x801f, 57}, + {0x8029, 57}, + {0xc038, 57}, + }, + /* 22 */ + { + {0x1a, 0}, + {0x1b, 0}, + {0x1d, 0}, + {0x1e, 0}, + {0x21, 0}, + {0x22, 0}, + {0x24, 0}, + {0x25, 0}, + {0x2b, 0}, + {0x2e, 0}, + {0x32, 0}, + {0x35, 0}, + {0x3a, 0}, + {0x3d, 0}, + {0x41, 0}, + {0x4044, 0}, + }, + /* 23 */ + { + {0xc000, 61}, + {0xc000, 65}, + {0xc000, 95}, + {0xc000, 98}, + {0xc000, 100}, + {0xc000, 102}, + {0xc000, 103}, + {0xc000, 104}, + {0xc000, 108}, + {0xc000, 109}, + {0xc000, 110}, + {0xc000, 112}, + {0xc000, 114}, + {0xc000, 117}, + {0x26, 0}, + {0x27, 0}, + }, + /* 24 */ + { + {0x8001, 61}, + {0xc016, 61}, + {0x8001, 65}, + {0xc016, 65}, + {0x8001, 95}, + {0xc016, 95}, + {0x8001, 98}, + {0xc016, 98}, + {0x8001, 100}, + {0xc016, 100}, + {0x8001, 102}, + {0xc016, 102}, + {0x8001, 103}, + {0xc016, 103}, + {0x8001, 104}, + {0xc016, 104}, + }, + /* 25 */ + { + {0x8002, 61}, + {0x8009, 61}, + {0x8017, 61}, + {0xc028, 61}, + {0x8002, 65}, + {0x8009, 65}, + {0x8017, 65}, + {0xc028, 65}, + {0x8002, 95}, + {0x8009, 95}, + {0x8017, 95}, + {0xc028, 95}, + {0x8002, 98}, + {0x8009, 98}, + {0x8017, 98}, + {0xc028, 98}, + }, + /* 26 */ + { + {0x8003, 61}, + {0x8006, 61}, + {0x800a, 61}, + {0x800f, 61}, + {0x8018, 61}, + {0x801f, 61}, + {0x8029, 61}, + {0xc038, 61}, + {0x8003, 65}, + {0x8006, 65}, + {0x800a, 65}, + {0x800f, 65}, + {0x8018, 65}, + {0x801f, 65}, + {0x8029, 65}, + {0xc038, 65}, + }, + /* 27 */ + { + {0x8003, 95}, + {0x8006, 95}, + {0x800a, 95}, + {0x800f, 95}, + {0x8018, 95}, + {0x801f, 95}, + {0x8029, 95}, + {0xc038, 95}, + {0x8003, 98}, + {0x8006, 98}, + {0x800a, 98}, + {0x800f, 98}, + {0x8018, 98}, + {0x801f, 98}, + {0x8029, 98}, + {0xc038, 98}, + }, + /* 28 */ + { + {0x8002, 100}, + {0x8009, 100}, + {0x8017, 100}, + {0xc028, 100}, + {0x8002, 102}, + {0x8009, 102}, + {0x8017, 102}, + {0xc028, 102}, + {0x8002, 103}, + {0x8009, 103}, + {0x8017, 103}, + {0xc028, 103}, + {0x8002, 104}, + {0x8009, 104}, + {0x8017, 104}, + {0xc028, 104}, + }, + /* 29 */ + { + {0x8003, 100}, + {0x8006, 100}, + {0x800a, 100}, + {0x800f, 100}, + {0x8018, 100}, + {0x801f, 100}, + {0x8029, 100}, + {0xc038, 100}, + {0x8003, 102}, + {0x8006, 102}, + {0x800a, 102}, + {0x800f, 102}, + {0x8018, 102}, + {0x801f, 102}, + {0x8029, 102}, + {0xc038, 102}, + }, + /* 30 */ + { + {0x8003, 103}, + {0x8006, 103}, + {0x800a, 103}, + {0x800f, 103}, + {0x8018, 103}, + {0x801f, 103}, + {0x8029, 103}, + {0xc038, 103}, + {0x8003, 104}, + {0x8006, 104}, + {0x800a, 104}, + {0x800f, 104}, + {0x8018, 104}, + {0x801f, 104}, + {0x8029, 104}, + {0xc038, 104}, + }, + /* 31 */ + { + {0x8001, 108}, + {0xc016, 108}, + {0x8001, 109}, + {0xc016, 109}, + {0x8001, 110}, + {0xc016, 110}, + {0x8001, 112}, + {0xc016, 112}, + {0x8001, 114}, + {0xc016, 114}, + {0x8001, 117}, + {0xc016, 117}, + {0xc000, 58}, + {0xc000, 66}, + {0xc000, 67}, + {0xc000, 68}, + }, + /* 32 */ + { + {0x8002, 108}, + {0x8009, 108}, + {0x8017, 108}, + {0xc028, 108}, + {0x8002, 109}, + {0x8009, 109}, + {0x8017, 109}, + {0xc028, 109}, + {0x8002, 110}, + {0x8009, 110}, + {0x8017, 110}, + {0xc028, 110}, + {0x8002, 112}, + {0x8009, 112}, + {0x8017, 112}, + {0xc028, 112}, + }, + /* 33 */ + { + {0x8003, 108}, + {0x8006, 108}, + {0x800a, 108}, + {0x800f, 108}, + {0x8018, 108}, + {0x801f, 108}, + {0x8029, 108}, + {0xc038, 108}, + {0x8003, 109}, + {0x8006, 109}, + {0x800a, 109}, + {0x800f, 109}, + {0x8018, 109}, + {0x801f, 109}, + {0x8029, 109}, + {0xc038, 109}, + }, + /* 34 */ + { + {0x8003, 110}, + {0x8006, 110}, + {0x800a, 110}, + {0x800f, 110}, + {0x8018, 110}, + {0x801f, 110}, + {0x8029, 110}, + {0xc038, 110}, + {0x8003, 112}, + {0x8006, 112}, + {0x800a, 112}, + {0x800f, 112}, + {0x8018, 112}, + {0x801f, 112}, + {0x8029, 112}, + {0xc038, 112}, + }, + /* 35 */ + { + {0x8002, 114}, + {0x8009, 114}, + {0x8017, 114}, + {0xc028, 114}, + {0x8002, 117}, + {0x8009, 117}, + {0x8017, 117}, + {0xc028, 117}, + {0x8001, 58}, + {0xc016, 58}, + {0x8001, 66}, + {0xc016, 66}, + {0x8001, 67}, + {0xc016, 67}, + {0x8001, 68}, + {0xc016, 68}, + }, + /* 36 */ + { + {0x8003, 114}, + {0x8006, 114}, + {0x800a, 114}, + {0x800f, 114}, + {0x8018, 114}, + {0x801f, 114}, + {0x8029, 114}, + {0xc038, 114}, + {0x8003, 117}, + {0x8006, 117}, + {0x800a, 117}, + {0x800f, 117}, + {0x8018, 117}, + {0x801f, 117}, + {0x8029, 117}, + {0xc038, 117}, + }, + /* 37 */ + { + {0x8002, 58}, + {0x8009, 58}, + {0x8017, 58}, + {0xc028, 58}, + {0x8002, 66}, + {0x8009, 66}, + {0x8017, 66}, + {0xc028, 66}, + {0x8002, 67}, + {0x8009, 67}, + {0x8017, 67}, + {0xc028, 67}, + {0x8002, 68}, + {0x8009, 68}, + {0x8017, 68}, + {0xc028, 68}, + }, + /* 38 */ + { + {0x8003, 58}, + {0x8006, 58}, + {0x800a, 58}, + {0x800f, 58}, + {0x8018, 58}, + {0x801f, 58}, + {0x8029, 58}, + {0xc038, 58}, + {0x8003, 66}, + {0x8006, 66}, + {0x800a, 66}, + {0x800f, 66}, + {0x8018, 66}, + {0x801f, 66}, + {0x8029, 66}, + {0xc038, 66}, + }, + /* 39 */ + { + {0x8003, 67}, + {0x8006, 67}, + {0x800a, 67}, + {0x800f, 67}, + {0x8018, 67}, + {0x801f, 67}, + {0x8029, 67}, + {0xc038, 67}, + {0x8003, 68}, + {0x8006, 68}, + {0x800a, 68}, + {0x800f, 68}, + {0x8018, 68}, + {0x801f, 68}, + {0x8029, 68}, + {0xc038, 68}, + }, + /* 40 */ + { + {0x2c, 0}, + {0x2d, 0}, + {0x2f, 0}, + {0x30, 0}, + {0x33, 0}, + {0x34, 0}, + {0x36, 0}, + {0x37, 0}, + {0x3b, 0}, + {0x3c, 0}, + {0x3e, 0}, + {0x3f, 0}, + {0x42, 0}, + {0x43, 0}, + {0x45, 0}, + {0x4048, 0}, + }, + /* 41 */ + { + {0xc000, 69}, + {0xc000, 70}, + {0xc000, 71}, + {0xc000, 72}, + {0xc000, 73}, + {0xc000, 74}, + {0xc000, 75}, + {0xc000, 76}, + {0xc000, 77}, + {0xc000, 78}, + {0xc000, 79}, + {0xc000, 80}, + {0xc000, 81}, + {0xc000, 82}, + {0xc000, 83}, + {0xc000, 84}, + }, + /* 42 */ + { + {0x8001, 69}, + {0xc016, 69}, + {0x8001, 70}, + {0xc016, 70}, + {0x8001, 71}, + {0xc016, 71}, + {0x8001, 72}, + {0xc016, 72}, + {0x8001, 73}, + {0xc016, 73}, + {0x8001, 74}, + {0xc016, 74}, + {0x8001, 75}, + {0xc016, 75}, + {0x8001, 76}, + {0xc016, 76}, + }, + /* 43 */ + { + {0x8002, 69}, + {0x8009, 69}, + {0x8017, 69}, + {0xc028, 69}, + {0x8002, 70}, + {0x8009, 70}, + {0x8017, 70}, + {0xc028, 70}, + {0x8002, 71}, + {0x8009, 71}, + {0x8017, 71}, + {0xc028, 71}, + {0x8002, 72}, + {0x8009, 72}, + {0x8017, 72}, + {0xc028, 72}, + }, + /* 44 */ + { + {0x8003, 69}, + {0x8006, 69}, + {0x800a, 69}, + {0x800f, 69}, + {0x8018, 69}, + {0x801f, 69}, + {0x8029, 69}, + {0xc038, 69}, + {0x8003, 70}, + {0x8006, 70}, + {0x800a, 70}, + {0x800f, 70}, + {0x8018, 70}, + {0x801f, 70}, + {0x8029, 70}, + {0xc038, 70}, + }, + /* 45 */ + { + {0x8003, 71}, + {0x8006, 71}, + {0x800a, 71}, + {0x800f, 71}, + {0x8018, 71}, + {0x801f, 71}, + {0x8029, 71}, + {0xc038, 71}, + {0x8003, 72}, + {0x8006, 72}, + {0x800a, 72}, + {0x800f, 72}, + {0x8018, 72}, + {0x801f, 72}, + {0x8029, 72}, + {0xc038, 72}, + }, + /* 46 */ + { + {0x8002, 73}, + {0x8009, 73}, + {0x8017, 73}, + {0xc028, 73}, + {0x8002, 74}, + {0x8009, 74}, + {0x8017, 74}, + {0xc028, 74}, + {0x8002, 75}, + {0x8009, 75}, + {0x8017, 75}, + {0xc028, 75}, + {0x8002, 76}, + {0x8009, 76}, + {0x8017, 76}, + {0xc028, 76}, + }, + /* 47 */ + { + {0x8003, 73}, + {0x8006, 73}, + {0x800a, 73}, + {0x800f, 73}, + {0x8018, 73}, + {0x801f, 73}, + {0x8029, 73}, + {0xc038, 73}, + {0x8003, 74}, + {0x8006, 74}, + {0x800a, 74}, + {0x800f, 74}, + {0x8018, 74}, + {0x801f, 74}, + {0x8029, 74}, + {0xc038, 74}, + }, + /* 48 */ + { + {0x8003, 75}, + {0x8006, 75}, + {0x800a, 75}, + {0x800f, 75}, + {0x8018, 75}, + {0x801f, 75}, + {0x8029, 75}, + {0xc038, 75}, + {0x8003, 76}, + {0x8006, 76}, + {0x800a, 76}, + {0x800f, 76}, + {0x8018, 76}, + {0x801f, 76}, + {0x8029, 76}, + {0xc038, 76}, + }, + /* 49 */ + { + {0x8001, 77}, + {0xc016, 77}, + {0x8001, 78}, + {0xc016, 78}, + {0x8001, 79}, + {0xc016, 79}, + {0x8001, 80}, + {0xc016, 80}, + {0x8001, 81}, + {0xc016, 81}, + {0x8001, 82}, + {0xc016, 82}, + {0x8001, 83}, + {0xc016, 83}, + {0x8001, 84}, + {0xc016, 84}, + }, + /* 50 */ + { + {0x8002, 77}, + {0x8009, 77}, + {0x8017, 77}, + {0xc028, 77}, + {0x8002, 78}, + {0x8009, 78}, + {0x8017, 78}, + {0xc028, 78}, + {0x8002, 79}, + {0x8009, 79}, + {0x8017, 79}, + {0xc028, 79}, + {0x8002, 80}, + {0x8009, 80}, + {0x8017, 80}, + {0xc028, 80}, + }, + /* 51 */ + { + {0x8003, 77}, + {0x8006, 77}, + {0x800a, 77}, + {0x800f, 77}, + {0x8018, 77}, + {0x801f, 77}, + {0x8029, 77}, + {0xc038, 77}, + {0x8003, 78}, + {0x8006, 78}, + {0x800a, 78}, + {0x800f, 78}, + {0x8018, 78}, + {0x801f, 78}, + {0x8029, 78}, + {0xc038, 78}, + }, + /* 52 */ + { + {0x8003, 79}, + {0x8006, 79}, + {0x800a, 79}, + {0x800f, 79}, + {0x8018, 79}, + {0x801f, 79}, + {0x8029, 79}, + {0xc038, 79}, + {0x8003, 80}, + {0x8006, 80}, + {0x800a, 80}, + {0x800f, 80}, + {0x8018, 80}, + {0x801f, 80}, + {0x8029, 80}, + {0xc038, 80}, + }, + /* 53 */ + { + {0x8002, 81}, + {0x8009, 81}, + {0x8017, 81}, + {0xc028, 81}, + {0x8002, 82}, + {0x8009, 82}, + {0x8017, 82}, + {0xc028, 82}, + {0x8002, 83}, + {0x8009, 83}, + {0x8017, 83}, + {0xc028, 83}, + {0x8002, 84}, + {0x8009, 84}, + {0x8017, 84}, + {0xc028, 84}, + }, + /* 54 */ + { + {0x8003, 81}, + {0x8006, 81}, + {0x800a, 81}, + {0x800f, 81}, + {0x8018, 81}, + {0x801f, 81}, + {0x8029, 81}, + {0xc038, 81}, + {0x8003, 82}, + {0x8006, 82}, + {0x800a, 82}, + {0x800f, 82}, + {0x8018, 82}, + {0x801f, 82}, + {0x8029, 82}, + {0xc038, 82}, + }, + /* 55 */ + { + {0x8003, 83}, + {0x8006, 83}, + {0x800a, 83}, + {0x800f, 83}, + {0x8018, 83}, + {0x801f, 83}, + {0x8029, 83}, + {0xc038, 83}, + {0x8003, 84}, + {0x8006, 84}, + {0x800a, 84}, + {0x800f, 84}, + {0x8018, 84}, + {0x801f, 84}, + {0x8029, 84}, + {0xc038, 84}, + }, + /* 56 */ + { + {0xc000, 85}, + {0xc000, 86}, + {0xc000, 87}, + {0xc000, 89}, + {0xc000, 106}, + {0xc000, 107}, + {0xc000, 113}, + {0xc000, 118}, + {0xc000, 119}, + {0xc000, 120}, + {0xc000, 121}, + {0xc000, 122}, + {0x46, 0}, + {0x47, 0}, + {0x49, 0}, + {0x404a, 0}, + }, + /* 57 */ + { + {0x8001, 85}, + {0xc016, 85}, + {0x8001, 86}, + {0xc016, 86}, + {0x8001, 87}, + {0xc016, 87}, + {0x8001, 89}, + {0xc016, 89}, + {0x8001, 106}, + {0xc016, 106}, + {0x8001, 107}, + {0xc016, 107}, + {0x8001, 113}, + {0xc016, 113}, + {0x8001, 118}, + {0xc016, 118}, + }, + /* 58 */ + { + {0x8002, 85}, + {0x8009, 85}, + {0x8017, 85}, + {0xc028, 85}, + {0x8002, 86}, + {0x8009, 86}, + {0x8017, 86}, + {0xc028, 86}, + {0x8002, 87}, + {0x8009, 87}, + {0x8017, 87}, + {0xc028, 87}, + {0x8002, 89}, + {0x8009, 89}, + {0x8017, 89}, + {0xc028, 89}, + }, + /* 59 */ + { + {0x8003, 85}, + {0x8006, 85}, + {0x800a, 85}, + {0x800f, 85}, + {0x8018, 85}, + {0x801f, 85}, + {0x8029, 85}, + {0xc038, 85}, + {0x8003, 86}, + {0x8006, 86}, + {0x800a, 86}, + {0x800f, 86}, + {0x8018, 86}, + {0x801f, 86}, + {0x8029, 86}, + {0xc038, 86}, + }, + /* 60 */ + { + {0x8003, 87}, + {0x8006, 87}, + {0x800a, 87}, + {0x800f, 87}, + {0x8018, 87}, + {0x801f, 87}, + {0x8029, 87}, + {0xc038, 87}, + {0x8003, 89}, + {0x8006, 89}, + {0x800a, 89}, + {0x800f, 89}, + {0x8018, 89}, + {0x801f, 89}, + {0x8029, 89}, + {0xc038, 89}, + }, + /* 61 */ + { + {0x8002, 106}, + {0x8009, 106}, + {0x8017, 106}, + {0xc028, 106}, + {0x8002, 107}, + {0x8009, 107}, + {0x8017, 107}, + {0xc028, 107}, + {0x8002, 113}, + {0x8009, 113}, + {0x8017, 113}, + {0xc028, 113}, + {0x8002, 118}, + {0x8009, 118}, + {0x8017, 118}, + {0xc028, 118}, + }, + /* 62 */ + { + {0x8003, 106}, + {0x8006, 106}, + {0x800a, 106}, + {0x800f, 106}, + {0x8018, 106}, + {0x801f, 106}, + {0x8029, 106}, + {0xc038, 106}, + {0x8003, 107}, + {0x8006, 107}, + {0x800a, 107}, + {0x800f, 107}, + {0x8018, 107}, + {0x801f, 107}, + {0x8029, 107}, + {0xc038, 107}, + }, + /* 63 */ + { + {0x8003, 113}, + {0x8006, 113}, + {0x800a, 113}, + {0x800f, 113}, + {0x8018, 113}, + {0x801f, 113}, + {0x8029, 113}, + {0xc038, 113}, + {0x8003, 118}, + {0x8006, 118}, + {0x800a, 118}, + {0x800f, 118}, + {0x8018, 118}, + {0x801f, 118}, + {0x8029, 118}, + {0xc038, 118}, + }, + /* 64 */ + { + {0x8001, 119}, + {0xc016, 119}, + {0x8001, 120}, + {0xc016, 120}, + {0x8001, 121}, + {0xc016, 121}, + {0x8001, 122}, + {0xc016, 122}, + {0xc000, 38}, + {0xc000, 42}, + {0xc000, 44}, + {0xc000, 59}, + {0xc000, 88}, + {0xc000, 90}, + {0x4b, 0}, + {0x4e, 0}, + }, + /* 65 */ + { + {0x8002, 119}, + {0x8009, 119}, + {0x8017, 119}, + {0xc028, 119}, + {0x8002, 120}, + {0x8009, 120}, + {0x8017, 120}, + {0xc028, 120}, + {0x8002, 121}, + {0x8009, 121}, + {0x8017, 121}, + {0xc028, 121}, + {0x8002, 122}, + {0x8009, 122}, + {0x8017, 122}, + {0xc028, 122}, + }, + /* 66 */ + { + {0x8003, 119}, + {0x8006, 119}, + {0x800a, 119}, + {0x800f, 119}, + {0x8018, 119}, + {0x801f, 119}, + {0x8029, 119}, + {0xc038, 119}, + {0x8003, 120}, + {0x8006, 120}, + {0x800a, 120}, + {0x800f, 120}, + {0x8018, 120}, + {0x801f, 120}, + {0x8029, 120}, + {0xc038, 120}, + }, + /* 67 */ + { + {0x8003, 121}, + {0x8006, 121}, + {0x800a, 121}, + {0x800f, 121}, + {0x8018, 121}, + {0x801f, 121}, + {0x8029, 121}, + {0xc038, 121}, + {0x8003, 122}, + {0x8006, 122}, + {0x800a, 122}, + {0x800f, 122}, + {0x8018, 122}, + {0x801f, 122}, + {0x8029, 122}, + {0xc038, 122}, + }, + /* 68 */ + { + {0x8001, 38}, + {0xc016, 38}, + {0x8001, 42}, + {0xc016, 42}, + {0x8001, 44}, + {0xc016, 44}, + {0x8001, 59}, + {0xc016, 59}, + {0x8001, 88}, + {0xc016, 88}, + {0x8001, 90}, + {0xc016, 90}, + {0x4c, 0}, + {0x4d, 0}, + {0x4f, 0}, + {0x51, 0}, + }, + /* 69 */ + { + {0x8002, 38}, + {0x8009, 38}, + {0x8017, 38}, + {0xc028, 38}, + {0x8002, 42}, + {0x8009, 42}, + {0x8017, 42}, + {0xc028, 42}, + {0x8002, 44}, + {0x8009, 44}, + {0x8017, 44}, + {0xc028, 44}, + {0x8002, 59}, + {0x8009, 59}, + {0x8017, 59}, + {0xc028, 59}, + }, + /* 70 */ + { + {0x8003, 38}, + {0x8006, 38}, + {0x800a, 38}, + {0x800f, 38}, + {0x8018, 38}, + {0x801f, 38}, + {0x8029, 38}, + {0xc038, 38}, + {0x8003, 42}, + {0x8006, 42}, + {0x800a, 42}, + {0x800f, 42}, + {0x8018, 42}, + {0x801f, 42}, + {0x8029, 42}, + {0xc038, 42}, + }, + /* 71 */ + { + {0x8003, 44}, + {0x8006, 44}, + {0x800a, 44}, + {0x800f, 44}, + {0x8018, 44}, + {0x801f, 44}, + {0x8029, 44}, + {0xc038, 44}, + {0x8003, 59}, + {0x8006, 59}, + {0x800a, 59}, + {0x800f, 59}, + {0x8018, 59}, + {0x801f, 59}, + {0x8029, 59}, + {0xc038, 59}, + }, + /* 72 */ + { + {0x8002, 88}, + {0x8009, 88}, + {0x8017, 88}, + {0xc028, 88}, + {0x8002, 90}, + {0x8009, 90}, + {0x8017, 90}, + {0xc028, 90}, + {0xc000, 33}, + {0xc000, 34}, + {0xc000, 40}, + {0xc000, 41}, + {0xc000, 63}, + {0x50, 0}, + {0x52, 0}, + {0x54, 0}, + }, + /* 73 */ + { + {0x8003, 88}, + {0x8006, 88}, + {0x800a, 88}, + {0x800f, 88}, + {0x8018, 88}, + {0x801f, 88}, + {0x8029, 88}, + {0xc038, 88}, + {0x8003, 90}, + {0x8006, 90}, + {0x800a, 90}, + {0x800f, 90}, + {0x8018, 90}, + {0x801f, 90}, + {0x8029, 90}, + {0xc038, 90}, + }, + /* 74 */ + { + {0x8001, 33}, + {0xc016, 33}, + {0x8001, 34}, + {0xc016, 34}, + {0x8001, 40}, + {0xc016, 40}, + {0x8001, 41}, + {0xc016, 41}, + {0x8001, 63}, + {0xc016, 63}, + {0xc000, 39}, + {0xc000, 43}, + {0xc000, 124}, + {0x53, 0}, + {0x55, 0}, + {0x58, 0}, + }, + /* 75 */ + { + {0x8002, 33}, + {0x8009, 33}, + {0x8017, 33}, + {0xc028, 33}, + {0x8002, 34}, + {0x8009, 34}, + {0x8017, 34}, + {0xc028, 34}, + {0x8002, 40}, + {0x8009, 40}, + {0x8017, 40}, + {0xc028, 40}, + {0x8002, 41}, + {0x8009, 41}, + {0x8017, 41}, + {0xc028, 41}, + }, + /* 76 */ + { + {0x8003, 33}, + {0x8006, 33}, + {0x800a, 33}, + {0x800f, 33}, + {0x8018, 33}, + {0x801f, 33}, + {0x8029, 33}, + {0xc038, 33}, + {0x8003, 34}, + {0x8006, 34}, + {0x800a, 34}, + {0x800f, 34}, + {0x8018, 34}, + {0x801f, 34}, + {0x8029, 34}, + {0xc038, 34}, + }, + /* 77 */ + { + {0x8003, 40}, + {0x8006, 40}, + {0x800a, 40}, + {0x800f, 40}, + {0x8018, 40}, + {0x801f, 40}, + {0x8029, 40}, + {0xc038, 40}, + {0x8003, 41}, + {0x8006, 41}, + {0x800a, 41}, + {0x800f, 41}, + {0x8018, 41}, + {0x801f, 41}, + {0x8029, 41}, + {0xc038, 41}, + }, + /* 78 */ + { + {0x8002, 63}, + {0x8009, 63}, + {0x8017, 63}, + {0xc028, 63}, + {0x8001, 39}, + {0xc016, 39}, + {0x8001, 43}, + {0xc016, 43}, + {0x8001, 124}, + {0xc016, 124}, + {0xc000, 35}, + {0xc000, 62}, + {0x56, 0}, + {0x57, 0}, + {0x59, 0}, + {0x5a, 0}, + }, + /* 79 */ + { + {0x8003, 63}, + {0x8006, 63}, + {0x800a, 63}, + {0x800f, 63}, + {0x8018, 63}, + {0x801f, 63}, + {0x8029, 63}, + {0xc038, 63}, + {0x8002, 39}, + {0x8009, 39}, + {0x8017, 39}, + {0xc028, 39}, + {0x8002, 43}, + {0x8009, 43}, + {0x8017, 43}, + {0xc028, 43}, + }, + /* 80 */ + { + {0x8003, 39}, + {0x8006, 39}, + {0x800a, 39}, + {0x800f, 39}, + {0x8018, 39}, + {0x801f, 39}, + {0x8029, 39}, + {0xc038, 39}, + {0x8003, 43}, + {0x8006, 43}, + {0x800a, 43}, + {0x800f, 43}, + {0x8018, 43}, + {0x801f, 43}, + {0x8029, 43}, + {0xc038, 43}, + }, + /* 81 */ + { + {0x8002, 124}, + {0x8009, 124}, + {0x8017, 124}, + {0xc028, 124}, + {0x8001, 35}, + {0xc016, 35}, + {0x8001, 62}, + {0xc016, 62}, + {0xc000, 0}, + {0xc000, 36}, + {0xc000, 64}, + {0xc000, 91}, + {0xc000, 93}, + {0xc000, 126}, + {0x5b, 0}, + {0x5c, 0}, + }, + /* 82 */ + { + {0x8003, 124}, + {0x8006, 124}, + {0x800a, 124}, + {0x800f, 124}, + {0x8018, 124}, + {0x801f, 124}, + {0x8029, 124}, + {0xc038, 124}, + {0x8002, 35}, + {0x8009, 35}, + {0x8017, 35}, + {0xc028, 35}, + {0x8002, 62}, + {0x8009, 62}, + {0x8017, 62}, + {0xc028, 62}, + }, + /* 83 */ + { + {0x8003, 35}, + {0x8006, 35}, + {0x800a, 35}, + {0x800f, 35}, + {0x8018, 35}, + {0x801f, 35}, + {0x8029, 35}, + {0xc038, 35}, + {0x8003, 62}, + {0x8006, 62}, + {0x800a, 62}, + {0x800f, 62}, + {0x8018, 62}, + {0x801f, 62}, + {0x8029, 62}, + {0xc038, 62}, + }, + /* 84 */ + { + {0x8001, 0}, + {0xc016, 0}, + {0x8001, 36}, + {0xc016, 36}, + {0x8001, 64}, + {0xc016, 64}, + {0x8001, 91}, + {0xc016, 91}, + {0x8001, 93}, + {0xc016, 93}, + {0x8001, 126}, + {0xc016, 126}, + {0xc000, 94}, + {0xc000, 125}, + {0x5d, 0}, + {0x5e, 0}, + }, + /* 85 */ + { + {0x8002, 0}, + {0x8009, 0}, + {0x8017, 0}, + {0xc028, 0}, + {0x8002, 36}, + {0x8009, 36}, + {0x8017, 36}, + {0xc028, 36}, + {0x8002, 64}, + {0x8009, 64}, + {0x8017, 64}, + {0xc028, 64}, + {0x8002, 91}, + {0x8009, 91}, + {0x8017, 91}, + {0xc028, 91}, + }, + /* 86 */ + { + {0x8003, 0}, + {0x8006, 0}, + {0x800a, 0}, + {0x800f, 0}, + {0x8018, 0}, + {0x801f, 0}, + {0x8029, 0}, + {0xc038, 0}, + {0x8003, 36}, + {0x8006, 36}, + {0x800a, 36}, + {0x800f, 36}, + {0x8018, 36}, + {0x801f, 36}, + {0x8029, 36}, + {0xc038, 36}, + }, + /* 87 */ + { + {0x8003, 64}, + {0x8006, 64}, + {0x800a, 64}, + {0x800f, 64}, + {0x8018, 64}, + {0x801f, 64}, + {0x8029, 64}, + {0xc038, 64}, + {0x8003, 91}, + {0x8006, 91}, + {0x800a, 91}, + {0x800f, 91}, + {0x8018, 91}, + {0x801f, 91}, + {0x8029, 91}, + {0xc038, 91}, + }, + /* 88 */ + { + {0x8002, 93}, + {0x8009, 93}, + {0x8017, 93}, + {0xc028, 93}, + {0x8002, 126}, + {0x8009, 126}, + {0x8017, 126}, + {0xc028, 126}, + {0x8001, 94}, + {0xc016, 94}, + {0x8001, 125}, + {0xc016, 125}, + {0xc000, 60}, + {0xc000, 96}, + {0xc000, 123}, + {0x5f, 0}, + }, + /* 89 */ + { + {0x8003, 93}, + {0x8006, 93}, + {0x800a, 93}, + {0x800f, 93}, + {0x8018, 93}, + {0x801f, 93}, + {0x8029, 93}, + {0xc038, 93}, + {0x8003, 126}, + {0x8006, 126}, + {0x800a, 126}, + {0x800f, 126}, + {0x8018, 126}, + {0x801f, 126}, + {0x8029, 126}, + {0xc038, 126}, + }, + /* 90 */ + { + {0x8002, 94}, + {0x8009, 94}, + {0x8017, 94}, + {0xc028, 94}, + {0x8002, 125}, + {0x8009, 125}, + {0x8017, 125}, + {0xc028, 125}, + {0x8001, 60}, + {0xc016, 60}, + {0x8001, 96}, + {0xc016, 96}, + {0x8001, 123}, + {0xc016, 123}, + {0x60, 0}, + {0x6e, 0}, + }, + /* 91 */ + { + {0x8003, 94}, + {0x8006, 94}, + {0x800a, 94}, + {0x800f, 94}, + {0x8018, 94}, + {0x801f, 94}, + {0x8029, 94}, + {0xc038, 94}, + {0x8003, 125}, + {0x8006, 125}, + {0x800a, 125}, + {0x800f, 125}, + {0x8018, 125}, + {0x801f, 125}, + {0x8029, 125}, + {0xc038, 125}, + }, + /* 92 */ + { + {0x8002, 60}, + {0x8009, 60}, + {0x8017, 60}, + {0xc028, 60}, + {0x8002, 96}, + {0x8009, 96}, + {0x8017, 96}, + {0xc028, 96}, + {0x8002, 123}, + {0x8009, 123}, + {0x8017, 123}, + {0xc028, 123}, + {0x61, 0}, + {0x65, 0}, + {0x6f, 0}, + {0x85, 0}, + }, + /* 93 */ + { + {0x8003, 60}, + {0x8006, 60}, + {0x800a, 60}, + {0x800f, 60}, + {0x8018, 60}, + {0x801f, 60}, + {0x8029, 60}, + {0xc038, 60}, + {0x8003, 96}, + {0x8006, 96}, + {0x800a, 96}, + {0x800f, 96}, + {0x8018, 96}, + {0x801f, 96}, + {0x8029, 96}, + {0xc038, 96}, + }, + /* 94 */ + { + {0x8003, 123}, + {0x8006, 123}, + {0x800a, 123}, + {0x800f, 123}, + {0x8018, 123}, + {0x801f, 123}, + {0x8029, 123}, + {0xc038, 123}, + {0x62, 0}, + {0x63, 0}, + {0x66, 0}, + {0x69, 0}, + {0x70, 0}, + {0x77, 0}, + {0x86, 0}, + {0x99, 0}, + }, + /* 95 */ + { + {0xc000, 92}, + {0xc000, 195}, + {0xc000, 208}, + {0x64, 0}, + {0x67, 0}, + {0x68, 0}, + {0x6a, 0}, + {0x6b, 0}, + {0x71, 0}, + {0x74, 0}, + {0x78, 0}, + {0x7e, 0}, + {0x87, 0}, + {0x8e, 0}, + {0x9a, 0}, + {0xa9, 0}, + }, + /* 96 */ + { + {0x8001, 92}, + {0xc016, 92}, + {0x8001, 195}, + {0xc016, 195}, + {0x8001, 208}, + {0xc016, 208}, + {0xc000, 128}, + {0xc000, 130}, + {0xc000, 131}, + {0xc000, 162}, + {0xc000, 184}, + {0xc000, 194}, + {0xc000, 224}, + {0xc000, 226}, + {0x6c, 0}, + {0x6d, 0}, + }, + /* 97 */ + { + {0x8002, 92}, + {0x8009, 92}, + {0x8017, 92}, + {0xc028, 92}, + {0x8002, 195}, + {0x8009, 195}, + {0x8017, 195}, + {0xc028, 195}, + {0x8002, 208}, + {0x8009, 208}, + {0x8017, 208}, + {0xc028, 208}, + {0x8001, 128}, + {0xc016, 128}, + {0x8001, 130}, + {0xc016, 130}, + }, + /* 98 */ + { + {0x8003, 92}, + {0x8006, 92}, + {0x800a, 92}, + {0x800f, 92}, + {0x8018, 92}, + {0x801f, 92}, + {0x8029, 92}, + {0xc038, 92}, + {0x8003, 195}, + {0x8006, 195}, + {0x800a, 195}, + {0x800f, 195}, + {0x8018, 195}, + {0x801f, 195}, + {0x8029, 195}, + {0xc038, 195}, + }, + /* 99 */ + { + {0x8003, 208}, + {0x8006, 208}, + {0x800a, 208}, + {0x800f, 208}, + {0x8018, 208}, + {0x801f, 208}, + {0x8029, 208}, + {0xc038, 208}, + {0x8002, 128}, + {0x8009, 128}, + {0x8017, 128}, + {0xc028, 128}, + {0x8002, 130}, + {0x8009, 130}, + {0x8017, 130}, + {0xc028, 130}, + }, + /* 100 */ + { + {0x8003, 128}, + {0x8006, 128}, + {0x800a, 128}, + {0x800f, 128}, + {0x8018, 128}, + {0x801f, 128}, + {0x8029, 128}, + {0xc038, 128}, + {0x8003, 130}, + {0x8006, 130}, + {0x800a, 130}, + {0x800f, 130}, + {0x8018, 130}, + {0x801f, 130}, + {0x8029, 130}, + {0xc038, 130}, + }, + /* 101 */ + { + {0x8001, 131}, + {0xc016, 131}, + {0x8001, 162}, + {0xc016, 162}, + {0x8001, 184}, + {0xc016, 184}, + {0x8001, 194}, + {0xc016, 194}, + {0x8001, 224}, + {0xc016, 224}, + {0x8001, 226}, + {0xc016, 226}, + {0xc000, 153}, + {0xc000, 161}, + {0xc000, 167}, + {0xc000, 172}, + }, + /* 102 */ + { + {0x8002, 131}, + {0x8009, 131}, + {0x8017, 131}, + {0xc028, 131}, + {0x8002, 162}, + {0x8009, 162}, + {0x8017, 162}, + {0xc028, 162}, + {0x8002, 184}, + {0x8009, 184}, + {0x8017, 184}, + {0xc028, 184}, + {0x8002, 194}, + {0x8009, 194}, + {0x8017, 194}, + {0xc028, 194}, + }, + /* 103 */ + { + {0x8003, 131}, + {0x8006, 131}, + {0x800a, 131}, + {0x800f, 131}, + {0x8018, 131}, + {0x801f, 131}, + {0x8029, 131}, + {0xc038, 131}, + {0x8003, 162}, + {0x8006, 162}, + {0x800a, 162}, + {0x800f, 162}, + {0x8018, 162}, + {0x801f, 162}, + {0x8029, 162}, + {0xc038, 162}, + }, + /* 104 */ + { + {0x8003, 184}, + {0x8006, 184}, + {0x800a, 184}, + {0x800f, 184}, + {0x8018, 184}, + {0x801f, 184}, + {0x8029, 184}, + {0xc038, 184}, + {0x8003, 194}, + {0x8006, 194}, + {0x800a, 194}, + {0x800f, 194}, + {0x8018, 194}, + {0x801f, 194}, + {0x8029, 194}, + {0xc038, 194}, + }, + /* 105 */ + { + {0x8002, 224}, + {0x8009, 224}, + {0x8017, 224}, + {0xc028, 224}, + {0x8002, 226}, + {0x8009, 226}, + {0x8017, 226}, + {0xc028, 226}, + {0x8001, 153}, + {0xc016, 153}, + {0x8001, 161}, + {0xc016, 161}, + {0x8001, 167}, + {0xc016, 167}, + {0x8001, 172}, + {0xc016, 172}, + }, + /* 106 */ + { + {0x8003, 224}, + {0x8006, 224}, + {0x800a, 224}, + {0x800f, 224}, + {0x8018, 224}, + {0x801f, 224}, + {0x8029, 224}, + {0xc038, 224}, + {0x8003, 226}, + {0x8006, 226}, + {0x800a, 226}, + {0x800f, 226}, + {0x8018, 226}, + {0x801f, 226}, + {0x8029, 226}, + {0xc038, 226}, + }, + /* 107 */ + { + {0x8002, 153}, + {0x8009, 153}, + {0x8017, 153}, + {0xc028, 153}, + {0x8002, 161}, + {0x8009, 161}, + {0x8017, 161}, + {0xc028, 161}, + {0x8002, 167}, + {0x8009, 167}, + {0x8017, 167}, + {0xc028, 167}, + {0x8002, 172}, + {0x8009, 172}, + {0x8017, 172}, + {0xc028, 172}, + }, + /* 108 */ + { + {0x8003, 153}, + {0x8006, 153}, + {0x800a, 153}, + {0x800f, 153}, + {0x8018, 153}, + {0x801f, 153}, + {0x8029, 153}, + {0xc038, 153}, + {0x8003, 161}, + {0x8006, 161}, + {0x800a, 161}, + {0x800f, 161}, + {0x8018, 161}, + {0x801f, 161}, + {0x8029, 161}, + {0xc038, 161}, + }, + /* 109 */ + { + {0x8003, 167}, + {0x8006, 167}, + {0x800a, 167}, + {0x800f, 167}, + {0x8018, 167}, + {0x801f, 167}, + {0x8029, 167}, + {0xc038, 167}, + {0x8003, 172}, + {0x8006, 172}, + {0x800a, 172}, + {0x800f, 172}, + {0x8018, 172}, + {0x801f, 172}, + {0x8029, 172}, + {0xc038, 172}, + }, + /* 110 */ + { + {0x72, 0}, + {0x73, 0}, + {0x75, 0}, + {0x76, 0}, + {0x79, 0}, + {0x7b, 0}, + {0x7f, 0}, + {0x82, 0}, + {0x88, 0}, + {0x8b, 0}, + {0x8f, 0}, + {0x92, 0}, + {0x9b, 0}, + {0xa2, 0}, + {0xaa, 0}, + {0xb4, 0}, + }, + /* 111 */ + { + {0xc000, 176}, + {0xc000, 177}, + {0xc000, 179}, + {0xc000, 209}, + {0xc000, 216}, + {0xc000, 217}, + {0xc000, 227}, + {0xc000, 229}, + {0xc000, 230}, + {0x7a, 0}, + {0x7c, 0}, + {0x7d, 0}, + {0x80, 0}, + {0x81, 0}, + {0x83, 0}, + {0x84, 0}, + }, + /* 112 */ + { + {0x8001, 176}, + {0xc016, 176}, + {0x8001, 177}, + {0xc016, 177}, + {0x8001, 179}, + {0xc016, 179}, + {0x8001, 209}, + {0xc016, 209}, + {0x8001, 216}, + {0xc016, 216}, + {0x8001, 217}, + {0xc016, 217}, + {0x8001, 227}, + {0xc016, 227}, + {0x8001, 229}, + {0xc016, 229}, + }, + /* 113 */ + { + {0x8002, 176}, + {0x8009, 176}, + {0x8017, 176}, + {0xc028, 176}, + {0x8002, 177}, + {0x8009, 177}, + {0x8017, 177}, + {0xc028, 177}, + {0x8002, 179}, + {0x8009, 179}, + {0x8017, 179}, + {0xc028, 179}, + {0x8002, 209}, + {0x8009, 209}, + {0x8017, 209}, + {0xc028, 209}, + }, + /* 114 */ + { + {0x8003, 176}, + {0x8006, 176}, + {0x800a, 176}, + {0x800f, 176}, + {0x8018, 176}, + {0x801f, 176}, + {0x8029, 176}, + {0xc038, 176}, + {0x8003, 177}, + {0x8006, 177}, + {0x800a, 177}, + {0x800f, 177}, + {0x8018, 177}, + {0x801f, 177}, + {0x8029, 177}, + {0xc038, 177}, + }, + /* 115 */ + { + {0x8003, 179}, + {0x8006, 179}, + {0x800a, 179}, + {0x800f, 179}, + {0x8018, 179}, + {0x801f, 179}, + {0x8029, 179}, + {0xc038, 179}, + {0x8003, 209}, + {0x8006, 209}, + {0x800a, 209}, + {0x800f, 209}, + {0x8018, 209}, + {0x801f, 209}, + {0x8029, 209}, + {0xc038, 209}, + }, + /* 116 */ + { + {0x8002, 216}, + {0x8009, 216}, + {0x8017, 216}, + {0xc028, 216}, + {0x8002, 217}, + {0x8009, 217}, + {0x8017, 217}, + {0xc028, 217}, + {0x8002, 227}, + {0x8009, 227}, + {0x8017, 227}, + {0xc028, 227}, + {0x8002, 229}, + {0x8009, 229}, + {0x8017, 229}, + {0xc028, 229}, + }, + /* 117 */ + { + {0x8003, 216}, + {0x8006, 216}, + {0x800a, 216}, + {0x800f, 216}, + {0x8018, 216}, + {0x801f, 216}, + {0x8029, 216}, + {0xc038, 216}, + {0x8003, 217}, + {0x8006, 217}, + {0x800a, 217}, + {0x800f, 217}, + {0x8018, 217}, + {0x801f, 217}, + {0x8029, 217}, + {0xc038, 217}, + }, + /* 118 */ + { + {0x8003, 227}, + {0x8006, 227}, + {0x800a, 227}, + {0x800f, 227}, + {0x8018, 227}, + {0x801f, 227}, + {0x8029, 227}, + {0xc038, 227}, + {0x8003, 229}, + {0x8006, 229}, + {0x800a, 229}, + {0x800f, 229}, + {0x8018, 229}, + {0x801f, 229}, + {0x8029, 229}, + {0xc038, 229}, + }, + /* 119 */ + { + {0x8001, 230}, + {0xc016, 230}, + {0xc000, 129}, + {0xc000, 132}, + {0xc000, 133}, + {0xc000, 134}, + {0xc000, 136}, + {0xc000, 146}, + {0xc000, 154}, + {0xc000, 156}, + {0xc000, 160}, + {0xc000, 163}, + {0xc000, 164}, + {0xc000, 169}, + {0xc000, 170}, + {0xc000, 173}, + }, + /* 120 */ + { + {0x8002, 230}, + {0x8009, 230}, + {0x8017, 230}, + {0xc028, 230}, + {0x8001, 129}, + {0xc016, 129}, + {0x8001, 132}, + {0xc016, 132}, + {0x8001, 133}, + {0xc016, 133}, + {0x8001, 134}, + {0xc016, 134}, + {0x8001, 136}, + {0xc016, 136}, + {0x8001, 146}, + {0xc016, 146}, + }, + /* 121 */ + { + {0x8003, 230}, + {0x8006, 230}, + {0x800a, 230}, + {0x800f, 230}, + {0x8018, 230}, + {0x801f, 230}, + {0x8029, 230}, + {0xc038, 230}, + {0x8002, 129}, + {0x8009, 129}, + {0x8017, 129}, + {0xc028, 129}, + {0x8002, 132}, + {0x8009, 132}, + {0x8017, 132}, + {0xc028, 132}, + }, + /* 122 */ + { + {0x8003, 129}, + {0x8006, 129}, + {0x800a, 129}, + {0x800f, 129}, + {0x8018, 129}, + {0x801f, 129}, + {0x8029, 129}, + {0xc038, 129}, + {0x8003, 132}, + {0x8006, 132}, + {0x800a, 132}, + {0x800f, 132}, + {0x8018, 132}, + {0x801f, 132}, + {0x8029, 132}, + {0xc038, 132}, + }, + /* 123 */ + { + {0x8002, 133}, + {0x8009, 133}, + {0x8017, 133}, + {0xc028, 133}, + {0x8002, 134}, + {0x8009, 134}, + {0x8017, 134}, + {0xc028, 134}, + {0x8002, 136}, + {0x8009, 136}, + {0x8017, 136}, + {0xc028, 136}, + {0x8002, 146}, + {0x8009, 146}, + {0x8017, 146}, + {0xc028, 146}, + }, + /* 124 */ + { + {0x8003, 133}, + {0x8006, 133}, + {0x800a, 133}, + {0x800f, 133}, + {0x8018, 133}, + {0x801f, 133}, + {0x8029, 133}, + {0xc038, 133}, + {0x8003, 134}, + {0x8006, 134}, + {0x800a, 134}, + {0x800f, 134}, + {0x8018, 134}, + {0x801f, 134}, + {0x8029, 134}, + {0xc038, 134}, + }, + /* 125 */ + { + {0x8003, 136}, + {0x8006, 136}, + {0x800a, 136}, + {0x800f, 136}, + {0x8018, 136}, + {0x801f, 136}, + {0x8029, 136}, + {0xc038, 136}, + {0x8003, 146}, + {0x8006, 146}, + {0x800a, 146}, + {0x800f, 146}, + {0x8018, 146}, + {0x801f, 146}, + {0x8029, 146}, + {0xc038, 146}, + }, + /* 126 */ + { + {0x8001, 154}, + {0xc016, 154}, + {0x8001, 156}, + {0xc016, 156}, + {0x8001, 160}, + {0xc016, 160}, + {0x8001, 163}, + {0xc016, 163}, + {0x8001, 164}, + {0xc016, 164}, + {0x8001, 169}, + {0xc016, 169}, + {0x8001, 170}, + {0xc016, 170}, + {0x8001, 173}, + {0xc016, 173}, + }, + /* 127 */ + { + {0x8002, 154}, + {0x8009, 154}, + {0x8017, 154}, + {0xc028, 154}, + {0x8002, 156}, + {0x8009, 156}, + {0x8017, 156}, + {0xc028, 156}, + {0x8002, 160}, + {0x8009, 160}, + {0x8017, 160}, + {0xc028, 160}, + {0x8002, 163}, + {0x8009, 163}, + {0x8017, 163}, + {0xc028, 163}, + }, + /* 128 */ + { + {0x8003, 154}, + {0x8006, 154}, + {0x800a, 154}, + {0x800f, 154}, + {0x8018, 154}, + {0x801f, 154}, + {0x8029, 154}, + {0xc038, 154}, + {0x8003, 156}, + {0x8006, 156}, + {0x800a, 156}, + {0x800f, 156}, + {0x8018, 156}, + {0x801f, 156}, + {0x8029, 156}, + {0xc038, 156}, + }, + /* 129 */ + { + {0x8003, 160}, + {0x8006, 160}, + {0x800a, 160}, + {0x800f, 160}, + {0x8018, 160}, + {0x801f, 160}, + {0x8029, 160}, + {0xc038, 160}, + {0x8003, 163}, + {0x8006, 163}, + {0x800a, 163}, + {0x800f, 163}, + {0x8018, 163}, + {0x801f, 163}, + {0x8029, 163}, + {0xc038, 163}, + }, + /* 130 */ + { + {0x8002, 164}, + {0x8009, 164}, + {0x8017, 164}, + {0xc028, 164}, + {0x8002, 169}, + {0x8009, 169}, + {0x8017, 169}, + {0xc028, 169}, + {0x8002, 170}, + {0x8009, 170}, + {0x8017, 170}, + {0xc028, 170}, + {0x8002, 173}, + {0x8009, 173}, + {0x8017, 173}, + {0xc028, 173}, + }, + /* 131 */ + { + {0x8003, 164}, + {0x8006, 164}, + {0x800a, 164}, + {0x800f, 164}, + {0x8018, 164}, + {0x801f, 164}, + {0x8029, 164}, + {0xc038, 164}, + {0x8003, 169}, + {0x8006, 169}, + {0x800a, 169}, + {0x800f, 169}, + {0x8018, 169}, + {0x801f, 169}, + {0x8029, 169}, + {0xc038, 169}, + }, + /* 132 */ + { + {0x8003, 170}, + {0x8006, 170}, + {0x800a, 170}, + {0x800f, 170}, + {0x8018, 170}, + {0x801f, 170}, + {0x8029, 170}, + {0xc038, 170}, + {0x8003, 173}, + {0x8006, 173}, + {0x800a, 173}, + {0x800f, 173}, + {0x8018, 173}, + {0x801f, 173}, + {0x8029, 173}, + {0xc038, 173}, + }, + /* 133 */ + { + {0x89, 0}, + {0x8a, 0}, + {0x8c, 0}, + {0x8d, 0}, + {0x90, 0}, + {0x91, 0}, + {0x93, 0}, + {0x96, 0}, + {0x9c, 0}, + {0x9f, 0}, + {0xa3, 0}, + {0xa6, 0}, + {0xab, 0}, + {0xae, 0}, + {0xb5, 0}, + {0xbe, 0}, + }, + /* 134 */ + { + {0xc000, 178}, + {0xc000, 181}, + {0xc000, 185}, + {0xc000, 186}, + {0xc000, 187}, + {0xc000, 189}, + {0xc000, 190}, + {0xc000, 196}, + {0xc000, 198}, + {0xc000, 228}, + {0xc000, 232}, + {0xc000, 233}, + {0x94, 0}, + {0x95, 0}, + {0x97, 0}, + {0x98, 0}, + }, + /* 135 */ + { + {0x8001, 178}, + {0xc016, 178}, + {0x8001, 181}, + {0xc016, 181}, + {0x8001, 185}, + {0xc016, 185}, + {0x8001, 186}, + {0xc016, 186}, + {0x8001, 187}, + {0xc016, 187}, + {0x8001, 189}, + {0xc016, 189}, + {0x8001, 190}, + {0xc016, 190}, + {0x8001, 196}, + {0xc016, 196}, + }, + /* 136 */ + { + {0x8002, 178}, + {0x8009, 178}, + {0x8017, 178}, + {0xc028, 178}, + {0x8002, 181}, + {0x8009, 181}, + {0x8017, 181}, + {0xc028, 181}, + {0x8002, 185}, + {0x8009, 185}, + {0x8017, 185}, + {0xc028, 185}, + {0x8002, 186}, + {0x8009, 186}, + {0x8017, 186}, + {0xc028, 186}, + }, + /* 137 */ + { + {0x8003, 178}, + {0x8006, 178}, + {0x800a, 178}, + {0x800f, 178}, + {0x8018, 178}, + {0x801f, 178}, + {0x8029, 178}, + {0xc038, 178}, + {0x8003, 181}, + {0x8006, 181}, + {0x800a, 181}, + {0x800f, 181}, + {0x8018, 181}, + {0x801f, 181}, + {0x8029, 181}, + {0xc038, 181}, + }, + /* 138 */ + { + {0x8003, 185}, + {0x8006, 185}, + {0x800a, 185}, + {0x800f, 185}, + {0x8018, 185}, + {0x801f, 185}, + {0x8029, 185}, + {0xc038, 185}, + {0x8003, 186}, + {0x8006, 186}, + {0x800a, 186}, + {0x800f, 186}, + {0x8018, 186}, + {0x801f, 186}, + {0x8029, 186}, + {0xc038, 186}, + }, + /* 139 */ + { + {0x8002, 187}, + {0x8009, 187}, + {0x8017, 187}, + {0xc028, 187}, + {0x8002, 189}, + {0x8009, 189}, + {0x8017, 189}, + {0xc028, 189}, + {0x8002, 190}, + {0x8009, 190}, + {0x8017, 190}, + {0xc028, 190}, + {0x8002, 196}, + {0x8009, 196}, + {0x8017, 196}, + {0xc028, 196}, + }, + /* 140 */ + { + {0x8003, 187}, + {0x8006, 187}, + {0x800a, 187}, + {0x800f, 187}, + {0x8018, 187}, + {0x801f, 187}, + {0x8029, 187}, + {0xc038, 187}, + {0x8003, 189}, + {0x8006, 189}, + {0x800a, 189}, + {0x800f, 189}, + {0x8018, 189}, + {0x801f, 189}, + {0x8029, 189}, + {0xc038, 189}, + }, + /* 141 */ + { + {0x8003, 190}, + {0x8006, 190}, + {0x800a, 190}, + {0x800f, 190}, + {0x8018, 190}, + {0x801f, 190}, + {0x8029, 190}, + {0xc038, 190}, + {0x8003, 196}, + {0x8006, 196}, + {0x800a, 196}, + {0x800f, 196}, + {0x8018, 196}, + {0x801f, 196}, + {0x8029, 196}, + {0xc038, 196}, + }, + /* 142 */ + { + {0x8001, 198}, + {0xc016, 198}, + {0x8001, 228}, + {0xc016, 228}, + {0x8001, 232}, + {0xc016, 232}, + {0x8001, 233}, + {0xc016, 233}, + {0xc000, 1}, + {0xc000, 135}, + {0xc000, 137}, + {0xc000, 138}, + {0xc000, 139}, + {0xc000, 140}, + {0xc000, 141}, + {0xc000, 143}, + }, + /* 143 */ + { + {0x8002, 198}, + {0x8009, 198}, + {0x8017, 198}, + {0xc028, 198}, + {0x8002, 228}, + {0x8009, 228}, + {0x8017, 228}, + {0xc028, 228}, + {0x8002, 232}, + {0x8009, 232}, + {0x8017, 232}, + {0xc028, 232}, + {0x8002, 233}, + {0x8009, 233}, + {0x8017, 233}, + {0xc028, 233}, + }, + /* 144 */ + { + {0x8003, 198}, + {0x8006, 198}, + {0x800a, 198}, + {0x800f, 198}, + {0x8018, 198}, + {0x801f, 198}, + {0x8029, 198}, + {0xc038, 198}, + {0x8003, 228}, + {0x8006, 228}, + {0x800a, 228}, + {0x800f, 228}, + {0x8018, 228}, + {0x801f, 228}, + {0x8029, 228}, + {0xc038, 228}, + }, + /* 145 */ + { + {0x8003, 232}, + {0x8006, 232}, + {0x800a, 232}, + {0x800f, 232}, + {0x8018, 232}, + {0x801f, 232}, + {0x8029, 232}, + {0xc038, 232}, + {0x8003, 233}, + {0x8006, 233}, + {0x800a, 233}, + {0x800f, 233}, + {0x8018, 233}, + {0x801f, 233}, + {0x8029, 233}, + {0xc038, 233}, + }, + /* 146 */ + { + {0x8001, 1}, + {0xc016, 1}, + {0x8001, 135}, + {0xc016, 135}, + {0x8001, 137}, + {0xc016, 137}, + {0x8001, 138}, + {0xc016, 138}, + {0x8001, 139}, + {0xc016, 139}, + {0x8001, 140}, + {0xc016, 140}, + {0x8001, 141}, + {0xc016, 141}, + {0x8001, 143}, + {0xc016, 143}, + }, + /* 147 */ + { + {0x8002, 1}, + {0x8009, 1}, + {0x8017, 1}, + {0xc028, 1}, + {0x8002, 135}, + {0x8009, 135}, + {0x8017, 135}, + {0xc028, 135}, + {0x8002, 137}, + {0x8009, 137}, + {0x8017, 137}, + {0xc028, 137}, + {0x8002, 138}, + {0x8009, 138}, + {0x8017, 138}, + {0xc028, 138}, + }, + /* 148 */ + { + {0x8003, 1}, + {0x8006, 1}, + {0x800a, 1}, + {0x800f, 1}, + {0x8018, 1}, + {0x801f, 1}, + {0x8029, 1}, + {0xc038, 1}, + {0x8003, 135}, + {0x8006, 135}, + {0x800a, 135}, + {0x800f, 135}, + {0x8018, 135}, + {0x801f, 135}, + {0x8029, 135}, + {0xc038, 135}, + }, + /* 149 */ + { + {0x8003, 137}, + {0x8006, 137}, + {0x800a, 137}, + {0x800f, 137}, + {0x8018, 137}, + {0x801f, 137}, + {0x8029, 137}, + {0xc038, 137}, + {0x8003, 138}, + {0x8006, 138}, + {0x800a, 138}, + {0x800f, 138}, + {0x8018, 138}, + {0x801f, 138}, + {0x8029, 138}, + {0xc038, 138}, + }, + /* 150 */ + { + {0x8002, 139}, + {0x8009, 139}, + {0x8017, 139}, + {0xc028, 139}, + {0x8002, 140}, + {0x8009, 140}, + {0x8017, 140}, + {0xc028, 140}, + {0x8002, 141}, + {0x8009, 141}, + {0x8017, 141}, + {0xc028, 141}, + {0x8002, 143}, + {0x8009, 143}, + {0x8017, 143}, + {0xc028, 143}, + }, + /* 151 */ + { + {0x8003, 139}, + {0x8006, 139}, + {0x800a, 139}, + {0x800f, 139}, + {0x8018, 139}, + {0x801f, 139}, + {0x8029, 139}, + {0xc038, 139}, + {0x8003, 140}, + {0x8006, 140}, + {0x800a, 140}, + {0x800f, 140}, + {0x8018, 140}, + {0x801f, 140}, + {0x8029, 140}, + {0xc038, 140}, + }, + /* 152 */ + { + {0x8003, 141}, + {0x8006, 141}, + {0x800a, 141}, + {0x800f, 141}, + {0x8018, 141}, + {0x801f, 141}, + {0x8029, 141}, + {0xc038, 141}, + {0x8003, 143}, + {0x8006, 143}, + {0x800a, 143}, + {0x800f, 143}, + {0x8018, 143}, + {0x801f, 143}, + {0x8029, 143}, + {0xc038, 143}, + }, + /* 153 */ + { + {0x9d, 0}, + {0x9e, 0}, + {0xa0, 0}, + {0xa1, 0}, + {0xa4, 0}, + {0xa5, 0}, + {0xa7, 0}, + {0xa8, 0}, + {0xac, 0}, + {0xad, 0}, + {0xaf, 0}, + {0xb1, 0}, + {0xb6, 0}, + {0xb9, 0}, + {0xbf, 0}, + {0xcf, 0}, + }, + /* 154 */ + { + {0xc000, 147}, + {0xc000, 149}, + {0xc000, 150}, + {0xc000, 151}, + {0xc000, 152}, + {0xc000, 155}, + {0xc000, 157}, + {0xc000, 158}, + {0xc000, 165}, + {0xc000, 166}, + {0xc000, 168}, + {0xc000, 174}, + {0xc000, 175}, + {0xc000, 180}, + {0xc000, 182}, + {0xc000, 183}, + }, + /* 155 */ + { + {0x8001, 147}, + {0xc016, 147}, + {0x8001, 149}, + {0xc016, 149}, + {0x8001, 150}, + {0xc016, 150}, + {0x8001, 151}, + {0xc016, 151}, + {0x8001, 152}, + {0xc016, 152}, + {0x8001, 155}, + {0xc016, 155}, + {0x8001, 157}, + {0xc016, 157}, + {0x8001, 158}, + {0xc016, 158}, + }, + /* 156 */ + { + {0x8002, 147}, + {0x8009, 147}, + {0x8017, 147}, + {0xc028, 147}, + {0x8002, 149}, + {0x8009, 149}, + {0x8017, 149}, + {0xc028, 149}, + {0x8002, 150}, + {0x8009, 150}, + {0x8017, 150}, + {0xc028, 150}, + {0x8002, 151}, + {0x8009, 151}, + {0x8017, 151}, + {0xc028, 151}, + }, + /* 157 */ + { + {0x8003, 147}, + {0x8006, 147}, + {0x800a, 147}, + {0x800f, 147}, + {0x8018, 147}, + {0x801f, 147}, + {0x8029, 147}, + {0xc038, 147}, + {0x8003, 149}, + {0x8006, 149}, + {0x800a, 149}, + {0x800f, 149}, + {0x8018, 149}, + {0x801f, 149}, + {0x8029, 149}, + {0xc038, 149}, + }, + /* 158 */ + { + {0x8003, 150}, + {0x8006, 150}, + {0x800a, 150}, + {0x800f, 150}, + {0x8018, 150}, + {0x801f, 150}, + {0x8029, 150}, + {0xc038, 150}, + {0x8003, 151}, + {0x8006, 151}, + {0x800a, 151}, + {0x800f, 151}, + {0x8018, 151}, + {0x801f, 151}, + {0x8029, 151}, + {0xc038, 151}, + }, + /* 159 */ + { + {0x8002, 152}, + {0x8009, 152}, + {0x8017, 152}, + {0xc028, 152}, + {0x8002, 155}, + {0x8009, 155}, + {0x8017, 155}, + {0xc028, 155}, + {0x8002, 157}, + {0x8009, 157}, + {0x8017, 157}, + {0xc028, 157}, + {0x8002, 158}, + {0x8009, 158}, + {0x8017, 158}, + {0xc028, 158}, + }, + /* 160 */ + { + {0x8003, 152}, + {0x8006, 152}, + {0x800a, 152}, + {0x800f, 152}, + {0x8018, 152}, + {0x801f, 152}, + {0x8029, 152}, + {0xc038, 152}, + {0x8003, 155}, + {0x8006, 155}, + {0x800a, 155}, + {0x800f, 155}, + {0x8018, 155}, + {0x801f, 155}, + {0x8029, 155}, + {0xc038, 155}, + }, + /* 161 */ + { + {0x8003, 157}, + {0x8006, 157}, + {0x800a, 157}, + {0x800f, 157}, + {0x8018, 157}, + {0x801f, 157}, + {0x8029, 157}, + {0xc038, 157}, + {0x8003, 158}, + {0x8006, 158}, + {0x800a, 158}, + {0x800f, 158}, + {0x8018, 158}, + {0x801f, 158}, + {0x8029, 158}, + {0xc038, 158}, + }, + /* 162 */ + { + {0x8001, 165}, + {0xc016, 165}, + {0x8001, 166}, + {0xc016, 166}, + {0x8001, 168}, + {0xc016, 168}, + {0x8001, 174}, + {0xc016, 174}, + {0x8001, 175}, + {0xc016, 175}, + {0x8001, 180}, + {0xc016, 180}, + {0x8001, 182}, + {0xc016, 182}, + {0x8001, 183}, + {0xc016, 183}, + }, + /* 163 */ + { + {0x8002, 165}, + {0x8009, 165}, + {0x8017, 165}, + {0xc028, 165}, + {0x8002, 166}, + {0x8009, 166}, + {0x8017, 166}, + {0xc028, 166}, + {0x8002, 168}, + {0x8009, 168}, + {0x8017, 168}, + {0xc028, 168}, + {0x8002, 174}, + {0x8009, 174}, + {0x8017, 174}, + {0xc028, 174}, + }, + /* 164 */ + { + {0x8003, 165}, + {0x8006, 165}, + {0x800a, 165}, + {0x800f, 165}, + {0x8018, 165}, + {0x801f, 165}, + {0x8029, 165}, + {0xc038, 165}, + {0x8003, 166}, + {0x8006, 166}, + {0x800a, 166}, + {0x800f, 166}, + {0x8018, 166}, + {0x801f, 166}, + {0x8029, 166}, + {0xc038, 166}, + }, + /* 165 */ + { + {0x8003, 168}, + {0x8006, 168}, + {0x800a, 168}, + {0x800f, 168}, + {0x8018, 168}, + {0x801f, 168}, + {0x8029, 168}, + {0xc038, 168}, + {0x8003, 174}, + {0x8006, 174}, + {0x800a, 174}, + {0x800f, 174}, + {0x8018, 174}, + {0x801f, 174}, + {0x8029, 174}, + {0xc038, 174}, + }, + /* 166 */ + { + {0x8002, 175}, + {0x8009, 175}, + {0x8017, 175}, + {0xc028, 175}, + {0x8002, 180}, + {0x8009, 180}, + {0x8017, 180}, + {0xc028, 180}, + {0x8002, 182}, + {0x8009, 182}, + {0x8017, 182}, + {0xc028, 182}, + {0x8002, 183}, + {0x8009, 183}, + {0x8017, 183}, + {0xc028, 183}, + }, + /* 167 */ + { + {0x8003, 175}, + {0x8006, 175}, + {0x800a, 175}, + {0x800f, 175}, + {0x8018, 175}, + {0x801f, 175}, + {0x8029, 175}, + {0xc038, 175}, + {0x8003, 180}, + {0x8006, 180}, + {0x800a, 180}, + {0x800f, 180}, + {0x8018, 180}, + {0x801f, 180}, + {0x8029, 180}, + {0xc038, 180}, + }, + /* 168 */ + { + {0x8003, 182}, + {0x8006, 182}, + {0x800a, 182}, + {0x800f, 182}, + {0x8018, 182}, + {0x801f, 182}, + {0x8029, 182}, + {0xc038, 182}, + {0x8003, 183}, + {0x8006, 183}, + {0x800a, 183}, + {0x800f, 183}, + {0x8018, 183}, + {0x801f, 183}, + {0x8029, 183}, + {0xc038, 183}, + }, + /* 169 */ + { + {0xc000, 188}, + {0xc000, 191}, + {0xc000, 197}, + {0xc000, 231}, + {0xc000, 239}, + {0xb0, 0}, + {0xb2, 0}, + {0xb3, 0}, + {0xb7, 0}, + {0xb8, 0}, + {0xba, 0}, + {0xbb, 0}, + {0xc0, 0}, + {0xc7, 0}, + {0xd0, 0}, + {0xdf, 0}, + }, + /* 170 */ + { + {0x8001, 188}, + {0xc016, 188}, + {0x8001, 191}, + {0xc016, 191}, + {0x8001, 197}, + {0xc016, 197}, + {0x8001, 231}, + {0xc016, 231}, + {0x8001, 239}, + {0xc016, 239}, + {0xc000, 9}, + {0xc000, 142}, + {0xc000, 144}, + {0xc000, 145}, + {0xc000, 148}, + {0xc000, 159}, + }, + /* 171 */ + { + {0x8002, 188}, + {0x8009, 188}, + {0x8017, 188}, + {0xc028, 188}, + {0x8002, 191}, + {0x8009, 191}, + {0x8017, 191}, + {0xc028, 191}, + {0x8002, 197}, + {0x8009, 197}, + {0x8017, 197}, + {0xc028, 197}, + {0x8002, 231}, + {0x8009, 231}, + {0x8017, 231}, + {0xc028, 231}, + }, + /* 172 */ + { + {0x8003, 188}, + {0x8006, 188}, + {0x800a, 188}, + {0x800f, 188}, + {0x8018, 188}, + {0x801f, 188}, + {0x8029, 188}, + {0xc038, 188}, + {0x8003, 191}, + {0x8006, 191}, + {0x800a, 191}, + {0x800f, 191}, + {0x8018, 191}, + {0x801f, 191}, + {0x8029, 191}, + {0xc038, 191}, + }, + /* 173 */ + { + {0x8003, 197}, + {0x8006, 197}, + {0x800a, 197}, + {0x800f, 197}, + {0x8018, 197}, + {0x801f, 197}, + {0x8029, 197}, + {0xc038, 197}, + {0x8003, 231}, + {0x8006, 231}, + {0x800a, 231}, + {0x800f, 231}, + {0x8018, 231}, + {0x801f, 231}, + {0x8029, 231}, + {0xc038, 231}, + }, + /* 174 */ + { + {0x8002, 239}, + {0x8009, 239}, + {0x8017, 239}, + {0xc028, 239}, + {0x8001, 9}, + {0xc016, 9}, + {0x8001, 142}, + {0xc016, 142}, + {0x8001, 144}, + {0xc016, 144}, + {0x8001, 145}, + {0xc016, 145}, + {0x8001, 148}, + {0xc016, 148}, + {0x8001, 159}, + {0xc016, 159}, + }, + /* 175 */ + { + {0x8003, 239}, + {0x8006, 239}, + {0x800a, 239}, + {0x800f, 239}, + {0x8018, 239}, + {0x801f, 239}, + {0x8029, 239}, + {0xc038, 239}, + {0x8002, 9}, + {0x8009, 9}, + {0x8017, 9}, + {0xc028, 9}, + {0x8002, 142}, + {0x8009, 142}, + {0x8017, 142}, + {0xc028, 142}, + }, + /* 176 */ + { + {0x8003, 9}, + {0x8006, 9}, + {0x800a, 9}, + {0x800f, 9}, + {0x8018, 9}, + {0x801f, 9}, + {0x8029, 9}, + {0xc038, 9}, + {0x8003, 142}, + {0x8006, 142}, + {0x800a, 142}, + {0x800f, 142}, + {0x8018, 142}, + {0x801f, 142}, + {0x8029, 142}, + {0xc038, 142}, + }, + /* 177 */ + { + {0x8002, 144}, + {0x8009, 144}, + {0x8017, 144}, + {0xc028, 144}, + {0x8002, 145}, + {0x8009, 145}, + {0x8017, 145}, + {0xc028, 145}, + {0x8002, 148}, + {0x8009, 148}, + {0x8017, 148}, + {0xc028, 148}, + {0x8002, 159}, + {0x8009, 159}, + {0x8017, 159}, + {0xc028, 159}, + }, + /* 178 */ + { + {0x8003, 144}, + {0x8006, 144}, + {0x800a, 144}, + {0x800f, 144}, + {0x8018, 144}, + {0x801f, 144}, + {0x8029, 144}, + {0xc038, 144}, + {0x8003, 145}, + {0x8006, 145}, + {0x800a, 145}, + {0x800f, 145}, + {0x8018, 145}, + {0x801f, 145}, + {0x8029, 145}, + {0xc038, 145}, + }, + /* 179 */ + { + {0x8003, 148}, + {0x8006, 148}, + {0x800a, 148}, + {0x800f, 148}, + {0x8018, 148}, + {0x801f, 148}, + {0x8029, 148}, + {0xc038, 148}, + {0x8003, 159}, + {0x8006, 159}, + {0x800a, 159}, + {0x800f, 159}, + {0x8018, 159}, + {0x801f, 159}, + {0x8029, 159}, + {0xc038, 159}, + }, + /* 180 */ + { + {0xc000, 171}, + {0xc000, 206}, + {0xc000, 215}, + {0xc000, 225}, + {0xc000, 236}, + {0xc000, 237}, + {0xbc, 0}, + {0xbd, 0}, + {0xc1, 0}, + {0xc4, 0}, + {0xc8, 0}, + {0xcb, 0}, + {0xd1, 0}, + {0xd8, 0}, + {0xe0, 0}, + {0xee, 0}, + }, + /* 181 */ + { + {0x8001, 171}, + {0xc016, 171}, + {0x8001, 206}, + {0xc016, 206}, + {0x8001, 215}, + {0xc016, 215}, + {0x8001, 225}, + {0xc016, 225}, + {0x8001, 236}, + {0xc016, 236}, + {0x8001, 237}, + {0xc016, 237}, + {0xc000, 199}, + {0xc000, 207}, + {0xc000, 234}, + {0xc000, 235}, + }, + /* 182 */ + { + {0x8002, 171}, + {0x8009, 171}, + {0x8017, 171}, + {0xc028, 171}, + {0x8002, 206}, + {0x8009, 206}, + {0x8017, 206}, + {0xc028, 206}, + {0x8002, 215}, + {0x8009, 215}, + {0x8017, 215}, + {0xc028, 215}, + {0x8002, 225}, + {0x8009, 225}, + {0x8017, 225}, + {0xc028, 225}, + }, + /* 183 */ + { + {0x8003, 171}, + {0x8006, 171}, + {0x800a, 171}, + {0x800f, 171}, + {0x8018, 171}, + {0x801f, 171}, + {0x8029, 171}, + {0xc038, 171}, + {0x8003, 206}, + {0x8006, 206}, + {0x800a, 206}, + {0x800f, 206}, + {0x8018, 206}, + {0x801f, 206}, + {0x8029, 206}, + {0xc038, 206}, + }, + /* 184 */ + { + {0x8003, 215}, + {0x8006, 215}, + {0x800a, 215}, + {0x800f, 215}, + {0x8018, 215}, + {0x801f, 215}, + {0x8029, 215}, + {0xc038, 215}, + {0x8003, 225}, + {0x8006, 225}, + {0x800a, 225}, + {0x800f, 225}, + {0x8018, 225}, + {0x801f, 225}, + {0x8029, 225}, + {0xc038, 225}, + }, + /* 185 */ + { + {0x8002, 236}, + {0x8009, 236}, + {0x8017, 236}, + {0xc028, 236}, + {0x8002, 237}, + {0x8009, 237}, + {0x8017, 237}, + {0xc028, 237}, + {0x8001, 199}, + {0xc016, 199}, + {0x8001, 207}, + {0xc016, 207}, + {0x8001, 234}, + {0xc016, 234}, + {0x8001, 235}, + {0xc016, 235}, + }, + /* 186 */ + { + {0x8003, 236}, + {0x8006, 236}, + {0x800a, 236}, + {0x800f, 236}, + {0x8018, 236}, + {0x801f, 236}, + {0x8029, 236}, + {0xc038, 236}, + {0x8003, 237}, + {0x8006, 237}, + {0x800a, 237}, + {0x800f, 237}, + {0x8018, 237}, + {0x801f, 237}, + {0x8029, 237}, + {0xc038, 237}, + }, + /* 187 */ + { + {0x8002, 199}, + {0x8009, 199}, + {0x8017, 199}, + {0xc028, 199}, + {0x8002, 207}, + {0x8009, 207}, + {0x8017, 207}, + {0xc028, 207}, + {0x8002, 234}, + {0x8009, 234}, + {0x8017, 234}, + {0xc028, 234}, + {0x8002, 235}, + {0x8009, 235}, + {0x8017, 235}, + {0xc028, 235}, + }, + /* 188 */ + { + {0x8003, 199}, + {0x8006, 199}, + {0x800a, 199}, + {0x800f, 199}, + {0x8018, 199}, + {0x801f, 199}, + {0x8029, 199}, + {0xc038, 199}, + {0x8003, 207}, + {0x8006, 207}, + {0x800a, 207}, + {0x800f, 207}, + {0x8018, 207}, + {0x801f, 207}, + {0x8029, 207}, + {0xc038, 207}, + }, + /* 189 */ + { + {0x8003, 234}, + {0x8006, 234}, + {0x800a, 234}, + {0x800f, 234}, + {0x8018, 234}, + {0x801f, 234}, + {0x8029, 234}, + {0xc038, 234}, + {0x8003, 235}, + {0x8006, 235}, + {0x800a, 235}, + {0x800f, 235}, + {0x8018, 235}, + {0x801f, 235}, + {0x8029, 235}, + {0xc038, 235}, + }, + /* 190 */ + { + {0xc2, 0}, + {0xc3, 0}, + {0xc5, 0}, + {0xc6, 0}, + {0xc9, 0}, + {0xca, 0}, + {0xcc, 0}, + {0xcd, 0}, + {0xd2, 0}, + {0xd5, 0}, + {0xd9, 0}, + {0xdc, 0}, + {0xe1, 0}, + {0xe7, 0}, + {0xef, 0}, + {0xf6, 0}, + }, + /* 191 */ + { + {0xc000, 192}, + {0xc000, 193}, + {0xc000, 200}, + {0xc000, 201}, + {0xc000, 202}, + {0xc000, 205}, + {0xc000, 210}, + {0xc000, 213}, + {0xc000, 218}, + {0xc000, 219}, + {0xc000, 238}, + {0xc000, 240}, + {0xc000, 242}, + {0xc000, 243}, + {0xc000, 255}, + {0xce, 0}, + }, + /* 192 */ + { + {0x8001, 192}, + {0xc016, 192}, + {0x8001, 193}, + {0xc016, 193}, + {0x8001, 200}, + {0xc016, 200}, + {0x8001, 201}, + {0xc016, 201}, + {0x8001, 202}, + {0xc016, 202}, + {0x8001, 205}, + {0xc016, 205}, + {0x8001, 210}, + {0xc016, 210}, + {0x8001, 213}, + {0xc016, 213}, + }, + /* 193 */ + { + {0x8002, 192}, + {0x8009, 192}, + {0x8017, 192}, + {0xc028, 192}, + {0x8002, 193}, + {0x8009, 193}, + {0x8017, 193}, + {0xc028, 193}, + {0x8002, 200}, + {0x8009, 200}, + {0x8017, 200}, + {0xc028, 200}, + {0x8002, 201}, + {0x8009, 201}, + {0x8017, 201}, + {0xc028, 201}, + }, + /* 194 */ + { + {0x8003, 192}, + {0x8006, 192}, + {0x800a, 192}, + {0x800f, 192}, + {0x8018, 192}, + {0x801f, 192}, + {0x8029, 192}, + {0xc038, 192}, + {0x8003, 193}, + {0x8006, 193}, + {0x800a, 193}, + {0x800f, 193}, + {0x8018, 193}, + {0x801f, 193}, + {0x8029, 193}, + {0xc038, 193}, + }, + /* 195 */ + { + {0x8003, 200}, + {0x8006, 200}, + {0x800a, 200}, + {0x800f, 200}, + {0x8018, 200}, + {0x801f, 200}, + {0x8029, 200}, + {0xc038, 200}, + {0x8003, 201}, + {0x8006, 201}, + {0x800a, 201}, + {0x800f, 201}, + {0x8018, 201}, + {0x801f, 201}, + {0x8029, 201}, + {0xc038, 201}, + }, + /* 196 */ + { + {0x8002, 202}, + {0x8009, 202}, + {0x8017, 202}, + {0xc028, 202}, + {0x8002, 205}, + {0x8009, 205}, + {0x8017, 205}, + {0xc028, 205}, + {0x8002, 210}, + {0x8009, 210}, + {0x8017, 210}, + {0xc028, 210}, + {0x8002, 213}, + {0x8009, 213}, + {0x8017, 213}, + {0xc028, 213}, + }, + /* 197 */ + { + {0x8003, 202}, + {0x8006, 202}, + {0x800a, 202}, + {0x800f, 202}, + {0x8018, 202}, + {0x801f, 202}, + {0x8029, 202}, + {0xc038, 202}, + {0x8003, 205}, + {0x8006, 205}, + {0x800a, 205}, + {0x800f, 205}, + {0x8018, 205}, + {0x801f, 205}, + {0x8029, 205}, + {0xc038, 205}, + }, + /* 198 */ + { + {0x8003, 210}, + {0x8006, 210}, + {0x800a, 210}, + {0x800f, 210}, + {0x8018, 210}, + {0x801f, 210}, + {0x8029, 210}, + {0xc038, 210}, + {0x8003, 213}, + {0x8006, 213}, + {0x800a, 213}, + {0x800f, 213}, + {0x8018, 213}, + {0x801f, 213}, + {0x8029, 213}, + {0xc038, 213}, + }, + /* 199 */ + { + {0x8001, 218}, + {0xc016, 218}, + {0x8001, 219}, + {0xc016, 219}, + {0x8001, 238}, + {0xc016, 238}, + {0x8001, 240}, + {0xc016, 240}, + {0x8001, 242}, + {0xc016, 242}, + {0x8001, 243}, + {0xc016, 243}, + {0x8001, 255}, + {0xc016, 255}, + {0xc000, 203}, + {0xc000, 204}, + }, + /* 200 */ + { + {0x8002, 218}, + {0x8009, 218}, + {0x8017, 218}, + {0xc028, 218}, + {0x8002, 219}, + {0x8009, 219}, + {0x8017, 219}, + {0xc028, 219}, + {0x8002, 238}, + {0x8009, 238}, + {0x8017, 238}, + {0xc028, 238}, + {0x8002, 240}, + {0x8009, 240}, + {0x8017, 240}, + {0xc028, 240}, + }, + /* 201 */ + { + {0x8003, 218}, + {0x8006, 218}, + {0x800a, 218}, + {0x800f, 218}, + {0x8018, 218}, + {0x801f, 218}, + {0x8029, 218}, + {0xc038, 218}, + {0x8003, 219}, + {0x8006, 219}, + {0x800a, 219}, + {0x800f, 219}, + {0x8018, 219}, + {0x801f, 219}, + {0x8029, 219}, + {0xc038, 219}, + }, + /* 202 */ + { + {0x8003, 238}, + {0x8006, 238}, + {0x800a, 238}, + {0x800f, 238}, + {0x8018, 238}, + {0x801f, 238}, + {0x8029, 238}, + {0xc038, 238}, + {0x8003, 240}, + {0x8006, 240}, + {0x800a, 240}, + {0x800f, 240}, + {0x8018, 240}, + {0x801f, 240}, + {0x8029, 240}, + {0xc038, 240}, + }, + /* 203 */ + { + {0x8002, 242}, + {0x8009, 242}, + {0x8017, 242}, + {0xc028, 242}, + {0x8002, 243}, + {0x8009, 243}, + {0x8017, 243}, + {0xc028, 243}, + {0x8002, 255}, + {0x8009, 255}, + {0x8017, 255}, + {0xc028, 255}, + {0x8001, 203}, + {0xc016, 203}, + {0x8001, 204}, + {0xc016, 204}, + }, + /* 204 */ + { + {0x8003, 242}, + {0x8006, 242}, + {0x800a, 242}, + {0x800f, 242}, + {0x8018, 242}, + {0x801f, 242}, + {0x8029, 242}, + {0xc038, 242}, + {0x8003, 243}, + {0x8006, 243}, + {0x800a, 243}, + {0x800f, 243}, + {0x8018, 243}, + {0x801f, 243}, + {0x8029, 243}, + {0xc038, 243}, + }, + /* 205 */ + { + {0x8003, 255}, + {0x8006, 255}, + {0x800a, 255}, + {0x800f, 255}, + {0x8018, 255}, + {0x801f, 255}, + {0x8029, 255}, + {0xc038, 255}, + {0x8002, 203}, + {0x8009, 203}, + {0x8017, 203}, + {0xc028, 203}, + {0x8002, 204}, + {0x8009, 204}, + {0x8017, 204}, + {0xc028, 204}, + }, + /* 206 */ + { + {0x8003, 203}, + {0x8006, 203}, + {0x800a, 203}, + {0x800f, 203}, + {0x8018, 203}, + {0x801f, 203}, + {0x8029, 203}, + {0xc038, 203}, + {0x8003, 204}, + {0x8006, 204}, + {0x800a, 204}, + {0x800f, 204}, + {0x8018, 204}, + {0x801f, 204}, + {0x8029, 204}, + {0xc038, 204}, + }, + /* 207 */ + { + {0xd3, 0}, + {0xd4, 0}, + {0xd6, 0}, + {0xd7, 0}, + {0xda, 0}, + {0xdb, 0}, + {0xdd, 0}, + {0xde, 0}, + {0xe2, 0}, + {0xe4, 0}, + {0xe8, 0}, + {0xeb, 0}, + {0xf0, 0}, + {0xf3, 0}, + {0xf7, 0}, + {0xfa, 0}, + }, + /* 208 */ + { + {0xc000, 211}, + {0xc000, 212}, + {0xc000, 214}, + {0xc000, 221}, + {0xc000, 222}, + {0xc000, 223}, + {0xc000, 241}, + {0xc000, 244}, + {0xc000, 245}, + {0xc000, 246}, + {0xc000, 247}, + {0xc000, 248}, + {0xc000, 250}, + {0xc000, 251}, + {0xc000, 252}, + {0xc000, 253}, + }, + /* 209 */ + { + {0x8001, 211}, + {0xc016, 211}, + {0x8001, 212}, + {0xc016, 212}, + {0x8001, 214}, + {0xc016, 214}, + {0x8001, 221}, + {0xc016, 221}, + {0x8001, 222}, + {0xc016, 222}, + {0x8001, 223}, + {0xc016, 223}, + {0x8001, 241}, + {0xc016, 241}, + {0x8001, 244}, + {0xc016, 244}, + }, + /* 210 */ + { + {0x8002, 211}, + {0x8009, 211}, + {0x8017, 211}, + {0xc028, 211}, + {0x8002, 212}, + {0x8009, 212}, + {0x8017, 212}, + {0xc028, 212}, + {0x8002, 214}, + {0x8009, 214}, + {0x8017, 214}, + {0xc028, 214}, + {0x8002, 221}, + {0x8009, 221}, + {0x8017, 221}, + {0xc028, 221}, + }, + /* 211 */ + { + {0x8003, 211}, + {0x8006, 211}, + {0x800a, 211}, + {0x800f, 211}, + {0x8018, 211}, + {0x801f, 211}, + {0x8029, 211}, + {0xc038, 211}, + {0x8003, 212}, + {0x8006, 212}, + {0x800a, 212}, + {0x800f, 212}, + {0x8018, 212}, + {0x801f, 212}, + {0x8029, 212}, + {0xc038, 212}, + }, + /* 212 */ + { + {0x8003, 214}, + {0x8006, 214}, + {0x800a, 214}, + {0x800f, 214}, + {0x8018, 214}, + {0x801f, 214}, + {0x8029, 214}, + {0xc038, 214}, + {0x8003, 221}, + {0x8006, 221}, + {0x800a, 221}, + {0x800f, 221}, + {0x8018, 221}, + {0x801f, 221}, + {0x8029, 221}, + {0xc038, 221}, + }, + /* 213 */ + { + {0x8002, 222}, + {0x8009, 222}, + {0x8017, 222}, + {0xc028, 222}, + {0x8002, 223}, + {0x8009, 223}, + {0x8017, 223}, + {0xc028, 223}, + {0x8002, 241}, + {0x8009, 241}, + {0x8017, 241}, + {0xc028, 241}, + {0x8002, 244}, + {0x8009, 244}, + {0x8017, 244}, + {0xc028, 244}, + }, + /* 214 */ + { + {0x8003, 222}, + {0x8006, 222}, + {0x800a, 222}, + {0x800f, 222}, + {0x8018, 222}, + {0x801f, 222}, + {0x8029, 222}, + {0xc038, 222}, + {0x8003, 223}, + {0x8006, 223}, + {0x800a, 223}, + {0x800f, 223}, + {0x8018, 223}, + {0x801f, 223}, + {0x8029, 223}, + {0xc038, 223}, + }, + /* 215 */ + { + {0x8003, 241}, + {0x8006, 241}, + {0x800a, 241}, + {0x800f, 241}, + {0x8018, 241}, + {0x801f, 241}, + {0x8029, 241}, + {0xc038, 241}, + {0x8003, 244}, + {0x8006, 244}, + {0x800a, 244}, + {0x800f, 244}, + {0x8018, 244}, + {0x801f, 244}, + {0x8029, 244}, + {0xc038, 244}, + }, + /* 216 */ + { + {0x8001, 245}, + {0xc016, 245}, + {0x8001, 246}, + {0xc016, 246}, + {0x8001, 247}, + {0xc016, 247}, + {0x8001, 248}, + {0xc016, 248}, + {0x8001, 250}, + {0xc016, 250}, + {0x8001, 251}, + {0xc016, 251}, + {0x8001, 252}, + {0xc016, 252}, + {0x8001, 253}, + {0xc016, 253}, + }, + /* 217 */ + { + {0x8002, 245}, + {0x8009, 245}, + {0x8017, 245}, + {0xc028, 245}, + {0x8002, 246}, + {0x8009, 246}, + {0x8017, 246}, + {0xc028, 246}, + {0x8002, 247}, + {0x8009, 247}, + {0x8017, 247}, + {0xc028, 247}, + {0x8002, 248}, + {0x8009, 248}, + {0x8017, 248}, + {0xc028, 248}, + }, + /* 218 */ + { + {0x8003, 245}, + {0x8006, 245}, + {0x800a, 245}, + {0x800f, 245}, + {0x8018, 245}, + {0x801f, 245}, + {0x8029, 245}, + {0xc038, 245}, + {0x8003, 246}, + {0x8006, 246}, + {0x800a, 246}, + {0x800f, 246}, + {0x8018, 246}, + {0x801f, 246}, + {0x8029, 246}, + {0xc038, 246}, + }, + /* 219 */ + { + {0x8003, 247}, + {0x8006, 247}, + {0x800a, 247}, + {0x800f, 247}, + {0x8018, 247}, + {0x801f, 247}, + {0x8029, 247}, + {0xc038, 247}, + {0x8003, 248}, + {0x8006, 248}, + {0x800a, 248}, + {0x800f, 248}, + {0x8018, 248}, + {0x801f, 248}, + {0x8029, 248}, + {0xc038, 248}, + }, + /* 220 */ + { + {0x8002, 250}, + {0x8009, 250}, + {0x8017, 250}, + {0xc028, 250}, + {0x8002, 251}, + {0x8009, 251}, + {0x8017, 251}, + {0xc028, 251}, + {0x8002, 252}, + {0x8009, 252}, + {0x8017, 252}, + {0xc028, 252}, + {0x8002, 253}, + {0x8009, 253}, + {0x8017, 253}, + {0xc028, 253}, + }, + /* 221 */ + { + {0x8003, 250}, + {0x8006, 250}, + {0x800a, 250}, + {0x800f, 250}, + {0x8018, 250}, + {0x801f, 250}, + {0x8029, 250}, + {0xc038, 250}, + {0x8003, 251}, + {0x8006, 251}, + {0x800a, 251}, + {0x800f, 251}, + {0x8018, 251}, + {0x801f, 251}, + {0x8029, 251}, + {0xc038, 251}, + }, + /* 222 */ + { + {0x8003, 252}, + {0x8006, 252}, + {0x800a, 252}, + {0x800f, 252}, + {0x8018, 252}, + {0x801f, 252}, + {0x8029, 252}, + {0xc038, 252}, + {0x8003, 253}, + {0x8006, 253}, + {0x800a, 253}, + {0x800f, 253}, + {0x8018, 253}, + {0x801f, 253}, + {0x8029, 253}, + {0xc038, 253}, + }, + /* 223 */ + { + {0xc000, 254}, + {0xe3, 0}, + {0xe5, 0}, + {0xe6, 0}, + {0xe9, 0}, + {0xea, 0}, + {0xec, 0}, + {0xed, 0}, + {0xf1, 0}, + {0xf2, 0}, + {0xf4, 0}, + {0xf5, 0}, + {0xf8, 0}, + {0xf9, 0}, + {0xfb, 0}, + {0xfc, 0}, + }, + /* 224 */ + { + {0x8001, 254}, + {0xc016, 254}, + {0xc000, 2}, + {0xc000, 3}, + {0xc000, 4}, + {0xc000, 5}, + {0xc000, 6}, + {0xc000, 7}, + {0xc000, 8}, + {0xc000, 11}, + {0xc000, 12}, + {0xc000, 14}, + {0xc000, 15}, + {0xc000, 16}, + {0xc000, 17}, + {0xc000, 18}, + }, + /* 225 */ + { + {0x8002, 254}, + {0x8009, 254}, + {0x8017, 254}, + {0xc028, 254}, + {0x8001, 2}, + {0xc016, 2}, + {0x8001, 3}, + {0xc016, 3}, + {0x8001, 4}, + {0xc016, 4}, + {0x8001, 5}, + {0xc016, 5}, + {0x8001, 6}, + {0xc016, 6}, + {0x8001, 7}, + {0xc016, 7}, + }, + /* 226 */ + { + {0x8003, 254}, + {0x8006, 254}, + {0x800a, 254}, + {0x800f, 254}, + {0x8018, 254}, + {0x801f, 254}, + {0x8029, 254}, + {0xc038, 254}, + {0x8002, 2}, + {0x8009, 2}, + {0x8017, 2}, + {0xc028, 2}, + {0x8002, 3}, + {0x8009, 3}, + {0x8017, 3}, + {0xc028, 3}, + }, + /* 227 */ + { + {0x8003, 2}, + {0x8006, 2}, + {0x800a, 2}, + {0x800f, 2}, + {0x8018, 2}, + {0x801f, 2}, + {0x8029, 2}, + {0xc038, 2}, + {0x8003, 3}, + {0x8006, 3}, + {0x800a, 3}, + {0x800f, 3}, + {0x8018, 3}, + {0x801f, 3}, + {0x8029, 3}, + {0xc038, 3}, + }, + /* 228 */ + { + {0x8002, 4}, + {0x8009, 4}, + {0x8017, 4}, + {0xc028, 4}, + {0x8002, 5}, + {0x8009, 5}, + {0x8017, 5}, + {0xc028, 5}, + {0x8002, 6}, + {0x8009, 6}, + {0x8017, 6}, + {0xc028, 6}, + {0x8002, 7}, + {0x8009, 7}, + {0x8017, 7}, + {0xc028, 7}, + }, + /* 229 */ + { + {0x8003, 4}, + {0x8006, 4}, + {0x800a, 4}, + {0x800f, 4}, + {0x8018, 4}, + {0x801f, 4}, + {0x8029, 4}, + {0xc038, 4}, + {0x8003, 5}, + {0x8006, 5}, + {0x800a, 5}, + {0x800f, 5}, + {0x8018, 5}, + {0x801f, 5}, + {0x8029, 5}, + {0xc038, 5}, + }, + /* 230 */ + { + {0x8003, 6}, + {0x8006, 6}, + {0x800a, 6}, + {0x800f, 6}, + {0x8018, 6}, + {0x801f, 6}, + {0x8029, 6}, + {0xc038, 6}, + {0x8003, 7}, + {0x8006, 7}, + {0x800a, 7}, + {0x800f, 7}, + {0x8018, 7}, + {0x801f, 7}, + {0x8029, 7}, + {0xc038, 7}, + }, + /* 231 */ + { + {0x8001, 8}, + {0xc016, 8}, + {0x8001, 11}, + {0xc016, 11}, + {0x8001, 12}, + {0xc016, 12}, + {0x8001, 14}, + {0xc016, 14}, + {0x8001, 15}, + {0xc016, 15}, + {0x8001, 16}, + {0xc016, 16}, + {0x8001, 17}, + {0xc016, 17}, + {0x8001, 18}, + {0xc016, 18}, + }, + /* 232 */ + { + {0x8002, 8}, + {0x8009, 8}, + {0x8017, 8}, + {0xc028, 8}, + {0x8002, 11}, + {0x8009, 11}, + {0x8017, 11}, + {0xc028, 11}, + {0x8002, 12}, + {0x8009, 12}, + {0x8017, 12}, + {0xc028, 12}, + {0x8002, 14}, + {0x8009, 14}, + {0x8017, 14}, + {0xc028, 14}, + }, + /* 233 */ + { + {0x8003, 8}, + {0x8006, 8}, + {0x800a, 8}, + {0x800f, 8}, + {0x8018, 8}, + {0x801f, 8}, + {0x8029, 8}, + {0xc038, 8}, + {0x8003, 11}, + {0x8006, 11}, + {0x800a, 11}, + {0x800f, 11}, + {0x8018, 11}, + {0x801f, 11}, + {0x8029, 11}, + {0xc038, 11}, + }, + /* 234 */ + { + {0x8003, 12}, + {0x8006, 12}, + {0x800a, 12}, + {0x800f, 12}, + {0x8018, 12}, + {0x801f, 12}, + {0x8029, 12}, + {0xc038, 12}, + {0x8003, 14}, + {0x8006, 14}, + {0x800a, 14}, + {0x800f, 14}, + {0x8018, 14}, + {0x801f, 14}, + {0x8029, 14}, + {0xc038, 14}, + }, + /* 235 */ + { + {0x8002, 15}, + {0x8009, 15}, + {0x8017, 15}, + {0xc028, 15}, + {0x8002, 16}, + {0x8009, 16}, + {0x8017, 16}, + {0xc028, 16}, + {0x8002, 17}, + {0x8009, 17}, + {0x8017, 17}, + {0xc028, 17}, + {0x8002, 18}, + {0x8009, 18}, + {0x8017, 18}, + {0xc028, 18}, + }, + /* 236 */ + { + {0x8003, 15}, + {0x8006, 15}, + {0x800a, 15}, + {0x800f, 15}, + {0x8018, 15}, + {0x801f, 15}, + {0x8029, 15}, + {0xc038, 15}, + {0x8003, 16}, + {0x8006, 16}, + {0x800a, 16}, + {0x800f, 16}, + {0x8018, 16}, + {0x801f, 16}, + {0x8029, 16}, + {0xc038, 16}, + }, + /* 237 */ + { + {0x8003, 17}, + {0x8006, 17}, + {0x800a, 17}, + {0x800f, 17}, + {0x8018, 17}, + {0x801f, 17}, + {0x8029, 17}, + {0xc038, 17}, + {0x8003, 18}, + {0x8006, 18}, + {0x800a, 18}, + {0x800f, 18}, + {0x8018, 18}, + {0x801f, 18}, + {0x8029, 18}, + {0xc038, 18}, + }, + /* 238 */ + { + {0xc000, 19}, + {0xc000, 20}, + {0xc000, 21}, + {0xc000, 23}, + {0xc000, 24}, + {0xc000, 25}, + {0xc000, 26}, + {0xc000, 27}, + {0xc000, 28}, + {0xc000, 29}, + {0xc000, 30}, + {0xc000, 31}, + {0xc000, 127}, + {0xc000, 220}, + {0xc000, 249}, + {0xfd, 0}, + }, + /* 239 */ + { + {0x8001, 19}, + {0xc016, 19}, + {0x8001, 20}, + {0xc016, 20}, + {0x8001, 21}, + {0xc016, 21}, + {0x8001, 23}, + {0xc016, 23}, + {0x8001, 24}, + {0xc016, 24}, + {0x8001, 25}, + {0xc016, 25}, + {0x8001, 26}, + {0xc016, 26}, + {0x8001, 27}, + {0xc016, 27}, + }, + /* 240 */ + { + {0x8002, 19}, + {0x8009, 19}, + {0x8017, 19}, + {0xc028, 19}, + {0x8002, 20}, + {0x8009, 20}, + {0x8017, 20}, + {0xc028, 20}, + {0x8002, 21}, + {0x8009, 21}, + {0x8017, 21}, + {0xc028, 21}, + {0x8002, 23}, + {0x8009, 23}, + {0x8017, 23}, + {0xc028, 23}, + }, + /* 241 */ + { + {0x8003, 19}, + {0x8006, 19}, + {0x800a, 19}, + {0x800f, 19}, + {0x8018, 19}, + {0x801f, 19}, + {0x8029, 19}, + {0xc038, 19}, + {0x8003, 20}, + {0x8006, 20}, + {0x800a, 20}, + {0x800f, 20}, + {0x8018, 20}, + {0x801f, 20}, + {0x8029, 20}, + {0xc038, 20}, + }, + /* 242 */ + { + {0x8003, 21}, + {0x8006, 21}, + {0x800a, 21}, + {0x800f, 21}, + {0x8018, 21}, + {0x801f, 21}, + {0x8029, 21}, + {0xc038, 21}, + {0x8003, 23}, + {0x8006, 23}, + {0x800a, 23}, + {0x800f, 23}, + {0x8018, 23}, + {0x801f, 23}, + {0x8029, 23}, + {0xc038, 23}, + }, + /* 243 */ + { + {0x8002, 24}, + {0x8009, 24}, + {0x8017, 24}, + {0xc028, 24}, + {0x8002, 25}, + {0x8009, 25}, + {0x8017, 25}, + {0xc028, 25}, + {0x8002, 26}, + {0x8009, 26}, + {0x8017, 26}, + {0xc028, 26}, + {0x8002, 27}, + {0x8009, 27}, + {0x8017, 27}, + {0xc028, 27}, + }, + /* 244 */ + { + {0x8003, 24}, + {0x8006, 24}, + {0x800a, 24}, + {0x800f, 24}, + {0x8018, 24}, + {0x801f, 24}, + {0x8029, 24}, + {0xc038, 24}, + {0x8003, 25}, + {0x8006, 25}, + {0x800a, 25}, + {0x800f, 25}, + {0x8018, 25}, + {0x801f, 25}, + {0x8029, 25}, + {0xc038, 25}, + }, + /* 245 */ + { + {0x8003, 26}, + {0x8006, 26}, + {0x800a, 26}, + {0x800f, 26}, + {0x8018, 26}, + {0x801f, 26}, + {0x8029, 26}, + {0xc038, 26}, + {0x8003, 27}, + {0x8006, 27}, + {0x800a, 27}, + {0x800f, 27}, + {0x8018, 27}, + {0x801f, 27}, + {0x8029, 27}, + {0xc038, 27}, + }, + /* 246 */ + { + {0x8001, 28}, + {0xc016, 28}, + {0x8001, 29}, + {0xc016, 29}, + {0x8001, 30}, + {0xc016, 30}, + {0x8001, 31}, + {0xc016, 31}, + {0x8001, 127}, + {0xc016, 127}, + {0x8001, 220}, + {0xc016, 220}, + {0x8001, 249}, + {0xc016, 249}, + {0xfe, 0}, + {0xff, 0}, + }, + /* 247 */ + { + {0x8002, 28}, + {0x8009, 28}, + {0x8017, 28}, + {0xc028, 28}, + {0x8002, 29}, + {0x8009, 29}, + {0x8017, 29}, + {0xc028, 29}, + {0x8002, 30}, + {0x8009, 30}, + {0x8017, 30}, + {0xc028, 30}, + {0x8002, 31}, + {0x8009, 31}, + {0x8017, 31}, + {0xc028, 31}, + }, + /* 248 */ + { + {0x8003, 28}, + {0x8006, 28}, + {0x800a, 28}, + {0x800f, 28}, + {0x8018, 28}, + {0x801f, 28}, + {0x8029, 28}, + {0xc038, 28}, + {0x8003, 29}, + {0x8006, 29}, + {0x800a, 29}, + {0x800f, 29}, + {0x8018, 29}, + {0x801f, 29}, + {0x8029, 29}, + {0xc038, 29}, + }, + /* 249 */ + { + {0x8003, 30}, + {0x8006, 30}, + {0x800a, 30}, + {0x800f, 30}, + {0x8018, 30}, + {0x801f, 30}, + {0x8029, 30}, + {0xc038, 30}, + {0x8003, 31}, + {0x8006, 31}, + {0x800a, 31}, + {0x800f, 31}, + {0x8018, 31}, + {0x801f, 31}, + {0x8029, 31}, + {0xc038, 31}, + }, + /* 250 */ + { + {0x8002, 127}, + {0x8009, 127}, + {0x8017, 127}, + {0xc028, 127}, + {0x8002, 220}, + {0x8009, 220}, + {0x8017, 220}, + {0xc028, 220}, + {0x8002, 249}, + {0x8009, 249}, + {0x8017, 249}, + {0xc028, 249}, + {0xc000, 10}, + {0xc000, 13}, + {0xc000, 22}, + {0x100, 0}, + }, + /* 251 */ + { + {0x8003, 127}, + {0x8006, 127}, + {0x800a, 127}, + {0x800f, 127}, + {0x8018, 127}, + {0x801f, 127}, + {0x8029, 127}, + {0xc038, 127}, + {0x8003, 220}, + {0x8006, 220}, + {0x800a, 220}, + {0x800f, 220}, + {0x8018, 220}, + {0x801f, 220}, + {0x8029, 220}, + {0xc038, 220}, + }, + /* 252 */ + { + {0x8003, 249}, + {0x8006, 249}, + {0x800a, 249}, + {0x800f, 249}, + {0x8018, 249}, + {0x801f, 249}, + {0x8029, 249}, + {0xc038, 249}, + {0x8001, 10}, + {0xc016, 10}, + {0x8001, 13}, + {0xc016, 13}, + {0x8001, 22}, + {0xc016, 22}, + {0x100, 0}, + {0x100, 0}, + }, + /* 253 */ + { + {0x8002, 10}, + {0x8009, 10}, + {0x8017, 10}, + {0xc028, 10}, + {0x8002, 13}, + {0x8009, 13}, + {0x8017, 13}, + {0xc028, 13}, + {0x8002, 22}, + {0x8009, 22}, + {0x8017, 22}, + {0xc028, 22}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + }, + /* 254 */ + { + {0x8003, 10}, + {0x8006, 10}, + {0x800a, 10}, + {0x800f, 10}, + {0x8018, 10}, + {0x801f, 10}, + {0x8029, 10}, + {0xc038, 10}, + {0x8003, 13}, + {0x8006, 13}, + {0x800a, 13}, + {0x800f, 13}, + {0x8018, 13}, + {0x801f, 13}, + {0x8029, 13}, + {0xc038, 13}, + }, + /* 255 */ + { + {0x8003, 22}, + {0x8006, 22}, + {0x800a, 22}, + {0x800f, 22}, + {0x8018, 22}, + {0x801f, 22}, + {0x8029, 22}, + {0xc038, 22}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + }, + /* 256 */ + { + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + {0x100, 0}, + }, +}; diff --git a/src/waltz/h2/test_h2.c b/src/waltz/h2/test_h2.c new file mode 100644 index 0000000000..693bd44ab7 --- /dev/null +++ b/src/waltz/h2/test_h2.c @@ -0,0 +1,36 @@ +#include "../../util/fd_util.h" + +#include "test_hpack.c" +#include "test_h2_rbuf.c" +#include "test_h2_hdr_match.c" +#include "test_h2_conn.c" +#include "test_h2_proto.c" + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + + fd_rng_t _rng[1]; fd_rng_t * rng = fd_rng_join( fd_rng_new( _rng, 0U, 0UL ) ); + + FD_LOG_NOTICE(( "Testing hpack" )); + test_hpack(); + + FD_LOG_NOTICE(( "Testing h2_buf" )); + test_h2_rbuf( rng ); + + FD_LOG_NOTICE(( "Testing h2_hdr_match" )); + test_h2_hdr_match(); + + FD_LOG_NOTICE(( "Testing h2_conn" )); + test_h2_conn(); + + FD_LOG_NOTICE(( "Testing h2_proto" )); + test_h2_proto(); + + fd_rng_delete( fd_rng_leave( rng ) ); + + FD_LOG_NOTICE(( "pass" )); + fd_halt(); + return 0; +} diff --git a/src/waltz/h2/test_h2_conn.c b/src/waltz/h2/test_h2_conn.c new file mode 100644 index 0000000000..e9014c9d1f --- /dev/null +++ b/src/waltz/h2/test_h2_conn.c @@ -0,0 +1,258 @@ +#include "fd_h2_callback.h" +#include "fd_h2_conn.h" +#include "../../util/sanitize/fd_asan.h" +#include "fd_h2_proto.h" + +struct test_h2_callback_rec { + uint cb_established_cnt; +}; + +typedef struct test_h2_callback_rec test_h2_callback_rec_t; + +static test_h2_callback_rec_t cb_rec; + +static void +test_cb_conn_established( fd_h2_conn_t * conn ) { + (void)conn; + cb_rec.cb_established_cnt++; +} + +/* test_h2_client_handshake exercises various client-side handshake + state logic. There are three possible successful client handshake + sequences: + + Sequence 1: + - Client: Preface, SETTINGS + - Server: SETTINGS + - Client: SETTINGS ACK + - Server: SETTINGS ACK + + Sequence 3: + - Client: Preface, SETTINGS + - Server: SETTINGS ACK + - Server: SETTINGS + - Client: SETTINGS ACK */ + +static void +test_h2_client_handshake( void ) { + uchar scratch[256]; + uchar rbuf_rx_b[128]; + uchar rbuf_tx_b[128]; + + fd_h2_conn_t conn[1]; + FD_TEST( fd_h2_conn_init_client( conn )==conn ); + conn->self_settings.initial_window_size = 65535U; + conn->self_settings.max_frame_size = 16384U; + conn->self_settings.max_header_list_size = 4096U; + conn->self_settings.max_concurrent_streams = 128U; + + fd_h2_callbacks_t cb[1]; + fd_h2_callbacks_init( cb ); + cb->conn_established = test_cb_conn_established; + + /* Verify that the client initiates the conn */ + + FD_TEST( conn->flags == FD_H2_CONN_FLAGS_CLIENT_INITIAL ); + + fd_h2_rbuf_t rbuf_tx[1]; + fd_h2_rbuf_init( rbuf_tx, rbuf_tx_b, sizeof(rbuf_tx_b) ); + fd_h2_tx_control( conn, rbuf_tx, cb ); + FD_TEST( fd_h2_rbuf_used_sz( rbuf_tx )==69 ); + uchar * hello = fd_h2_rbuf_pop( rbuf_tx, scratch, 69 ); + FD_TEST( fd_memeq( hello, "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", 24 ) ); + static uchar const settings_frame_expected[ 45 ] = { + /* payload size: 24 bytes */ + 0x00, 0x00, 0x24, + /* frame type: SETTINGS */ + 0x04, + /* flags: none */ + 0x00, + /* stream id: 0 */ + 0x00, 0x00, 0x00, 0x00, + + /* HEADER_TABLE_SIZE: 0 */ + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + /* ENABLE_PUSH: 0 */ + 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + /* MAX_CONCURRENT_STREAMS: 128 */ + 0x00, 0x03, 0x00, 0x00, 0x00, 0x80, + /* INITIAL_WINDOW_SIZE: 65535 */ + 0x00, 0x04, 0x00, 0x00, 0xff, 0xff, + /* MAX_FRAME_SIZE: 16384 */ + 0x00, 0x05, 0x00, 0x00, 0x40, 0x00, + /* MAX_HEADER_LIST_SIZE: 4096 */ + 0x00, 0x06, 0x00, 0x00, 0x10, 0x00 + }; + FD_TEST( fd_memeq( hello+24, settings_frame_expected, sizeof(settings_frame_expected) ) ); + + FD_TEST( conn->flags == (FD_H2_CONN_FLAGS_WAIT_SETTINGS_0 | FD_H2_CONN_FLAGS_WAIT_SETTINGS_ACK_0) ); + fd_h2_tx_control( conn, rbuf_tx, cb ); + fd_h2_tx_control( conn, rbuf_tx, cb ); + FD_TEST( fd_h2_rbuf_used_sz( rbuf_tx )==0 ); + + /* Server: SETTINGS, SETTINGS ACK */ + + static uchar const server_settings[ 45 ] = { + /* payload size: 36 bytes */ + 0x00, 0x00, 0x24, + /* frame type: SETTINGS */ + 0x04, + /* flags: none */ + 0x00, + /* stream id: 0 */ + 0x00, 0x00, 0x00, 0x00, + + /* HEADER_TABLE_SIZE: 2 */ + 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + /* ENABLE_PUSH: 1 */ + 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, + /* MAX_CONCURRENT_STREAMS: 256 */ + 0x00, 0x03, 0x00, 0x00, 0x01, 0x00, + /* INITIAL_WINDOW_SIZE: 131071 */ + 0x00, 0x04, 0x00, 0x01, 0xff, 0xff, + /* MAX_FRAME_SIZE: 32768 */ + 0x00, 0x05, 0x00, 0x00, 0x80, 0x00, + /* MAX_HEADER_LIST_SIZE: 8192 */ + 0x00, 0x06, 0x00, 0x00, 0x20, 0x00 + }; + fd_h2_rbuf_t rbuf_rx[1]; + FD_TEST( fd_h2_rbuf_init( rbuf_rx, rbuf_rx_b, sizeof(rbuf_rx_b) )==rbuf_rx ); + fd_h2_rbuf_push( rbuf_rx, server_settings, sizeof(server_settings) ); + fd_h2_rx( conn, rbuf_rx, rbuf_tx, scratch, sizeof(scratch), cb ); + FD_TEST( fd_h2_rbuf_used_sz( rbuf_tx )==9 ); + + static uchar const settings_ack_expected[ 9 ] = { + /* payload size: 0 bytes */ + 0x00, 0x00, 0x00, + /* frame type: SETTINGS */ + 0x04, + /* flags: ACK */ + 0x01, + /* stream id: 0 */ + 0x00, 0x00, 0x00, 0x00 + }; + uchar * settings_ack = fd_h2_rbuf_pop( rbuf_tx, scratch, 9UL ); + FD_TEST( fd_memeq( settings_ack, settings_ack_expected, sizeof(settings_ack_expected) ) ); + + FD_TEST( conn->flags == FD_H2_CONN_FLAGS_WAIT_SETTINGS_ACK_0 ); + fd_h2_tx_control( conn, rbuf_tx, cb ); + FD_TEST( fd_h2_rbuf_used_sz( rbuf_tx )==0 ); + + FD_TEST( cb_rec.cb_established_cnt==0 ); + fd_h2_rbuf_push( rbuf_rx, settings_ack_expected, sizeof(settings_ack_expected) ); + fd_h2_rx( conn, rbuf_rx, rbuf_tx, scratch, sizeof(scratch), cb ); + FD_TEST( fd_h2_rbuf_used_sz( rbuf_tx )==0 ); + FD_TEST( conn->flags == 0 ); + fd_h2_tx_control( conn, rbuf_tx, cb ); + FD_TEST( fd_h2_rbuf_used_sz( rbuf_tx )==0 ); + FD_TEST( cb_rec.cb_established_cnt==1 ); + + /* Retry the scenario, but this time: + Server: SETTINGS ACK, SETTINGS */ + + cb_rec.cb_established_cnt = 0; + FD_TEST( fd_h2_conn_init_client( conn )==conn ); + conn->self_settings.initial_window_size = 65535U; + conn->self_settings.max_frame_size = 16384U; + conn->self_settings.max_header_list_size = 4096U; + conn->self_settings.max_concurrent_streams = 128U; + + /* Pretend we just sent a preface and a settings frame, and are now + waiting on the server's response */ + conn->flags = FD_H2_CONN_FLAGS_WAIT_SETTINGS_0 | FD_H2_CONN_FLAGS_WAIT_SETTINGS_ACK_0; + conn->setting_tx = 1; + + fd_h2_rbuf_push( rbuf_rx, settings_ack_expected, sizeof(settings_ack_expected) ); + fd_h2_rx( conn, rbuf_rx, rbuf_tx, scratch, sizeof(scratch), cb ); + FD_TEST( fd_h2_rbuf_used_sz( rbuf_tx )==0 ); + FD_TEST( conn->flags == FD_H2_CONN_FLAGS_WAIT_SETTINGS_0 ); + FD_TEST( cb_rec.cb_established_cnt==0 ); + + fd_h2_rbuf_push( rbuf_rx, server_settings, sizeof(server_settings) ); + fd_h2_rx( conn, rbuf_rx, rbuf_tx, scratch, sizeof(scratch), cb ); + FD_TEST( fd_h2_rbuf_used_sz( rbuf_tx )==9 ); + settings_ack = fd_h2_rbuf_pop( rbuf_tx, scratch, 9UL ); + FD_TEST( fd_memeq( settings_ack, settings_ack_expected, sizeof(settings_ack_expected) ) ); + FD_TEST( conn->flags == 0 ); + FD_TEST( cb_rec.cb_established_cnt==1 ); +} + +static ulong test_h2_ping_tx_ack_cnt = 0UL; + +static void +test_h2_ping_ack( fd_h2_conn_t * conn ) { + (void)conn; + test_h2_ping_tx_ack_cnt++; +} + +static void +test_h2_ping_tx( void ) { + fd_h2_conn_t conn[1]; + FD_TEST( fd_h2_conn_init_client( conn )==conn ); + uchar scratch[128]; + conn->self_settings.max_frame_size = sizeof(scratch); + + fd_h2_callbacks_t cb[1]; + fd_h2_callbacks_init( cb ); + cb->ping_ack = test_h2_ping_ack; + + uchar rbuf_tx_b[128] = {0}; + fd_h2_rbuf_t rbuf_tx[1]; + fd_h2_rbuf_init( rbuf_tx, rbuf_tx_b, sizeof(rbuf_tx_b) ); + + /* Too many pending pings */ + conn->ping_tx = UCHAR_MAX; + FD_TEST( fd_h2_tx_ping( conn, rbuf_tx )==0 ); + conn->ping_tx = 0; + + /* rbuf_tx is full */ + fd_h2_rbuf_push( rbuf_tx, rbuf_tx_b, sizeof(rbuf_tx_b)-sizeof(fd_h2_ping_t)+1 ); + FD_TEST( fd_h2_tx_ping( conn, rbuf_tx )==0 ); + + /* Exactly enough space for a ping */ + fd_h2_rbuf_init( rbuf_tx, rbuf_tx_b, sizeof(rbuf_tx_b) ); + fd_h2_rbuf_push( rbuf_tx, rbuf_tx_b, sizeof(rbuf_tx_b)-sizeof(fd_h2_ping_t) ); + FD_TEST( fd_h2_tx_ping( conn, rbuf_tx )==1 ); + + /* Parse ping */ + fd_h2_rbuf_skip( rbuf_tx, sizeof(rbuf_tx_b)-sizeof(fd_h2_ping_t) ); + fd_h2_ping_t ping; + fd_h2_rbuf_pop_copy( rbuf_tx, &ping, sizeof(fd_h2_ping_t) ); + FD_TEST( ping.hdr.typlen == fd_h2_frame_typlen( FD_H2_FRAME_TYPE_PING, 8UL ) ); + FD_TEST( ping.hdr.flags == 0 ); + FD_TEST( ping.hdr.r_stream_id == 0 ); + FD_TEST( ping.payload == 0UL ); + + /* Acknowledge ping */ + fd_h2_ping_t ping_ack = { + .hdr = { + .typlen = fd_h2_frame_typlen( FD_H2_FRAME_TYPE_PING, 8UL ), + .flags = FD_H2_FLAG_ACK, + .r_stream_id = 0 + }, + .payload = 0UL + }; + + /* Ensure PING ACK callback is triggered */ + uchar rbuf_rx_b[128] = {0}; + fd_h2_rbuf_t rbuf_rx[1]; + fd_h2_rbuf_init( rbuf_rx, rbuf_rx_b, sizeof(rbuf_rx_b) ); + fd_h2_rbuf_push( rbuf_rx, &ping_ack, sizeof(fd_h2_ping_t) ); + FD_TEST( conn->ping_tx==1 ); + FD_TEST( test_h2_ping_tx_ack_cnt==0UL ); + fd_h2_rx( conn, rbuf_rx, rbuf_tx, scratch, sizeof(scratch), cb ); + FD_TEST( conn->ping_tx==0 ); + FD_TEST( test_h2_ping_tx_ack_cnt==1UL ); + + /* Unsolicited PING ACKs should be ignored */ + fd_h2_rbuf_push( rbuf_rx, &ping_ack, sizeof(fd_h2_ping_t) ); + fd_h2_rx( conn, rbuf_rx, rbuf_tx, scratch, sizeof(scratch), cb ); + FD_TEST( conn->ping_tx==0 ); + FD_TEST( test_h2_ping_tx_ack_cnt==1UL ); +} + +static void +test_h2_conn( void ) { + test_h2_client_handshake(); + test_h2_ping_tx(); +} diff --git a/src/waltz/h2/test_h2_hdr_match.c b/src/waltz/h2/test_h2_hdr_match.c new file mode 100644 index 0000000000..361f19f399 --- /dev/null +++ b/src/waltz/h2/test_h2_hdr_match.c @@ -0,0 +1,167 @@ +#include "fd_h2_hdr_match.h" +#include "fd_hpack.h" +#include "fd_hpack_private.h" + +#include /* fork */ +#include /* exit */ +#include /* wait */ +#include /* syscall */ + +FD_STATIC_ASSERT( FD_H2_HDR_UNKNOWN==0, num ); + +static void +check_hpack_idx( int idx, + char const * name, + ulong name_idx ) { + switch( idx ) { +# define _(idx,lit) \ + case idx: \ + FD_TEST( name_idx==sizeof(lit)-1 && fd_memeq( name, lit, sizeof(lit)-1 ) ); \ + break; + _( FD_H2_HDR_AUTHORITY, ":authority" ); + _( FD_H2_HDR_METHOD, ":method" ); + _( FD_H2_HDR_PATH, ":path" ); + _( FD_H2_HDR_SCHEME, ":scheme" ); + _( FD_H2_HDR_STATUS, ":status" ); + _( FD_H2_HDR_ACCEPT_CHARSET, "accept-charset" ); + _( FD_H2_HDR_ACCEPT_ENCODING, "accept-encoding" ); + _( FD_H2_HDR_ACCEPT_LANGUAGE, "accept-language" ); + _( FD_H2_HDR_ACCEPT_RANGES, "accept-ranges" ); + _( FD_H2_HDR_ACCEPT, "accept" ); + _( FD_H2_HDR_ACCESS_CONTROL_ALLOW_ORIGIN, "access-control-allow-origin" ); + _( FD_H2_HDR_AGE, "age" ); + _( FD_H2_HDR_ALLOW, "allow" ); + _( FD_H2_HDR_AUTHORIZATION, "authorization" ); + _( FD_H2_HDR_CACHE_CONTROL, "cache-control" ); + _( FD_H2_HDR_CONTENT_DISPOSITION, "content-disposition" ); + _( FD_H2_HDR_CONTENT_ENCODING, "content-encoding" ); + _( FD_H2_HDR_CONTENT_LANGUAGE, "content-language" ); + _( FD_H2_HDR_CONTENT_LENGTH, "content-length" ); + _( FD_H2_HDR_CONTENT_LOCATION, "content-location" ); + _( FD_H2_HDR_CONTENT_RANGE, "content-range" ); + _( FD_H2_HDR_CONTENT_TYPE, "content-type" ); + _( FD_H2_HDR_COOKIE, "cookie" ); + _( FD_H2_HDR_DATE, "date" ); + _( FD_H2_HDR_ETAG, "etag" ); + _( FD_H2_HDR_EXPECT, "expect" ); + _( FD_H2_HDR_EXPIRES, "expires" ); + _( FD_H2_HDR_FROM, "from" ); + _( FD_H2_HDR_HOST, "host" ); + _( FD_H2_HDR_IF_MATCH, "if-match" ); + _( FD_H2_HDR_IF_MODIFIED_SINCE, "if-modified-since" ); + _( FD_H2_HDR_IF_NONE_MATCH, "if-none-match" ); + _( FD_H2_HDR_IF_RANGE, "if-range" ); + _( FD_H2_HDR_IF_UNMODIFIED_SINCE, "if-unmodified-since" ); + _( FD_H2_HDR_LAST_MODIFIED, "last-modified" ); + _( FD_H2_HDR_LINK, "link" ); + _( FD_H2_HDR_LOCATION, "location" ); + _( FD_H2_HDR_MAX_FORWARDS, "max-forwards" ); + _( FD_H2_HDR_PROXY_AUTHENTICATE, "proxy-authenticate" ); + _( FD_H2_HDR_PROXY_AUTHORIZATION, "proxy-authorization" ); + _( FD_H2_HDR_RANGE, "range" ); + _( FD_H2_HDR_REFERER, "referer" ); + _( FD_H2_HDR_REFRESH, "refresh" ); + _( FD_H2_HDR_RETRY_AFTER, "retry-after" ); + _( FD_H2_HDR_SERVER, "server" ); + _( FD_H2_HDR_SET_COOKIE, "set-cookie" ); + _( FD_H2_HDR_STRICT_TRANSPORT_SECURITY, "strict-transport-security" ); + _( FD_H2_HDR_TRANSFER_ENCODING, "transfer-encoding" ); + _( FD_H2_HDR_USER_AGENT, "user-agent" ); + _( FD_H2_HDR_VARY, "vary" ); + _( FD_H2_HDR_VIA, "via" ); + _( FD_H2_HDR_WWW_AUTHENTICATE, "www-authenticate" ); + default: + FD_LOG_ERR(( "invalid idx %d", idx )); + } +# undef EXPECT_NAME +} + +void +test_h2_hdr_match( void ) { + fd_h2_hdr_matcher_t matcher[1]; + FD_TEST( fd_h2_hdr_matcher_init( matcher, 1UL )==matcher ); + + /* Test sanity checks */ + int log_lvl = fd_log_level_stderr(); + fd_log_level_stderr_set( 4 ); + FD_TEST( !fd_h2_hdr_matcher_init( NULL, 0UL ) ); + FD_TEST( !fd_h2_hdr_matcher_init( (uchar *)matcher+1, 0UL ) ); + fd_log_level_stderr_set( log_lvl ); + + /* Test query */ + fd_h2_hdr_match_seed = 123UL; + for( uint i=1U; i<=61U; i++ ) { + char const * name = fd_hpack_static_table[ i ].entry; + ulong name_len = fd_hpack_static_table[ i ].name_len; + + int idx = fd_h2_hdr_match( matcher, NULL, 0UL, FD_H2_HDR_HINT_NAME_INDEXED|i ); + check_hpack_idx( idx, name, name_len ); + + idx = fd_h2_hdr_match( matcher, name, name_len, 0UL ); + check_hpack_idx( idx, name, name_len ); + } + + FD_TEST( fd_h2_hdr_match( matcher, "sec-websocket-key", 17UL, 0U )==-53 ); + FD_TEST( fd_h2_hdr_match( matcher, "sec-websocket-extensions", 25UL, 0U )==-54 ); + FD_TEST( fd_h2_hdr_match( matcher, "sec-websocket-accept", 20UL, 0U )==-55 ); + FD_TEST( fd_h2_hdr_match( matcher, "sec-websocket-protocol", 22UL, 0U )==-56 ); + FD_TEST( fd_h2_hdr_match( matcher, "sec-websocket-version", 21UL, 0U )==-57 ); + + FD_TEST( !fd_h2_hdr_match( matcher, NULL, 0UL, 0U ) ); + + fd_h2_hdr_match_seed = 123UL; + FD_TEST( !fd_h2_hdr_match( matcher, "foo", 3UL, 0U ) ); + fd_h2_hdr_match_seed = 123UL; + fd_h2_hdr_matcher_insert( matcher, 1, "foo", 3UL ); + FD_TEST( matcher->entry_cnt==1 ); + fd_h2_hdr_matcher_insert( matcher, 1, "foo", 3UL ); + FD_TEST( matcher->entry_cnt==1 ); + fd_h2_hdr_match_seed = 123UL; + FD_TEST( fd_h2_hdr_match( matcher, "foo", 3UL, 0U )==1 ); + FD_TEST( fd_h2_hdr_match( matcher, "foo", 2UL, 0U )==0 ); + FD_TEST( fd_h2_hdr_match( matcher, "foo", 4UL, 0U )==0 ); + + /* Hash collision */ + static char const collision[8] = "\xae\x73\x65\x0d\x01\x00\x00\x00"; + fd_h2_hdr_matcher_insert( matcher, 2, collision, 8UL ); + fd_h2_hdr_match_entry_t const * entry1 = fd_h2_hdr_map_query_const( matcher->entry, (fd_h2_hdr_match_key_t){ .hdr="foo", .hdr_len=3 }, NULL ); + fd_h2_hdr_match_entry_t const * entry2 = fd_h2_hdr_map_query_const( matcher->entry, (fd_h2_hdr_match_key_t){ .hdr=collision, .hdr_len=8 }, NULL ); + FD_TEST( entry1->hash == entry2->hash ); + FD_TEST( fd_h2_hdr_match( matcher, "foo", 3UL, 0U )==1 ); + FD_TEST( fd_h2_hdr_match( matcher, collision, 8UL, 0U )==2 ); + FD_TEST( matcher->entry_cnt==2 ); + +#if FD_HAS_HOSTED && defined(__linux__) + #define FD_TEST_FAIL( ACTION ) do { \ + pid_t pid = fork(); \ + FD_TEST( pid >= 0 ); \ + if( pid==0 ) { \ + fd_log_enable_unclean_exit(); \ + fd_log_level_stderr_set( 5 ); \ + ACTION; \ + exit( 0 ); \ + } \ + int status = 0; \ + wait( &status ); \ + FD_TEST( WIFEXITED(status) && WEXITSTATUS(status)==1 ); \ + } while( 0 ) + + FD_TEST_FAIL( fd_h2_hdr_matcher_insert( matcher, 0, "bla", 3UL ) ); + FD_TEST_FAIL( fd_h2_hdr_matcher_insert( matcher, -1, "bla", 3UL ) ); + FD_TEST_FAIL( fd_h2_hdr_matcher_insert( matcher, 65536, "bla", 3UL ) ); + FD_TEST_FAIL( fd_h2_hdr_matcher_insert( matcher, 3, NULL, 0UL ) ); + FD_TEST_FAIL( fd_h2_hdr_matcher_insert( matcher, 3, NULL, ULONG_MAX ) ); +#endif + + /* Fill hash map */ + for( uint rem=FD_H2_HDR_MATCH_MAX-2; rem; rem-- ) { + char key[4]; FD_STORE( uint, key, rem ); + fd_h2_hdr_matcher_insert( matcher, 2, key, 4UL ); + } + FD_TEST( matcher->entry_cnt==FD_H2_HDR_MATCH_MAX ); +#if FD_HAS_HOSTED && defined(__linux__) + FD_TEST_FAIL( fd_h2_hdr_matcher_insert( matcher, 4, "overflow", 8UL ) ); +#endif + + fd_h2_hdr_matcher_fini( matcher ); +} diff --git a/src/waltz/h2/test_h2_proto.c b/src/waltz/h2/test_h2_proto.c new file mode 100644 index 0000000000..6d97dec1d1 --- /dev/null +++ b/src/waltz/h2/test_h2_proto.c @@ -0,0 +1,48 @@ +#include "fd_h2_proto.h" +#include "../../util/log/fd_log.h" + +/* https://www.iana.org/assignments/http2-parameters/http2-parameters.xhtml */ + +static void +test_h2_enum( void ) { + + /* Frame types */ + + FD_TEST( !strcmp( fd_h2_frame_name( 0x00u ), "DATA" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x01u ), "HEADERS" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x02u ), "PRIORITY" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x03u ), "RST_STREAM" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x04u ), "SETTINGS" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x05u ), "PUSH_PROMISE" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x06u ), "PING" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x07u ), "GOAWAY" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x08u ), "WINDOW_UPDATE" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x09u ), "CONTINUATION" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x0au ), "ALTSVC" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x0bu ), "unknown" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x0cu ), "ORIGIN" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x0du ), "unknown" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x0eu ), "unknown" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x0fu ), "unknown" ) ); + FD_TEST( !strcmp( fd_h2_frame_name( 0x10u ), "PRIORITY_UPDATE" ) ); + for( uint i=0x11u; i<=0xffu; i++ ) { + FD_TEST( !strcmp( fd_h2_frame_name( i ), "unknown" ) ); + } + + /* Settings */ + + FD_TEST( !strcmp( fd_h2_setting_name( 0x00 ), "reserved" ) ); + FD_TEST( !strcmp( fd_h2_setting_name( 0x01 ), "HEADER_TABLE_SIZE" ) ); + FD_TEST( !strcmp( fd_h2_setting_name( 0x02 ), "ENABLE_PUSH" ) ); + FD_TEST( !strcmp( fd_h2_setting_name( 0x03 ), "MAX_CONCURRENT_STREAMS" ) ); + FD_TEST( !strcmp( fd_h2_setting_name( 0x04 ), "INITIAL_WINDOW_SIZE" ) ); + FD_TEST( !strcmp( fd_h2_setting_name( 0x05 ), "MAX_FRAME_SIZE" ) ); + FD_TEST( !strcmp( fd_h2_setting_name( 0x06 ), "MAX_HEADER_LIST_SIZE" ) ); + FD_TEST( !strcmp( fd_h2_setting_name( 0x07 ), "unknown" ) ); + +} + +static void +test_h2_proto( void ) { + test_h2_enum(); +} diff --git a/src/waltz/h2/test_h2_rbuf.c b/src/waltz/h2/test_h2_rbuf.c new file mode 100644 index 0000000000..55edcff93f --- /dev/null +++ b/src/waltz/h2/test_h2_rbuf.c @@ -0,0 +1,87 @@ +#include "fd_h2_rbuf_sock.h" +#include "../../util/rng/fd_rng.h" + +void +test_h2_rbuf( fd_rng_t * rng ) { + uchar scratch[64]; + for( int j=0UL; j<64; j++ ) scratch[j] = (uchar)( 'A'+j ); + + uchar buf[64]; + fd_h2_rbuf_t rbuf[1]; + fd_h2_rbuf_init( rbuf, buf, sizeof(buf) ); + FD_TEST( fd_h2_rbuf_free_sz( rbuf )==64 ); + FD_TEST( fd_h2_rbuf_used_sz( rbuf )== 0 ); + + uchar shadow[64]; + ulong shadow_cons = 0UL; + ulong shadow_prod = 0UL; + + for( ulong iter=0UL; iter<10000000UL; iter++ ) { + FD_TEST( rbuf->lo >= buf && rbuf->lo < buf+sizeof(buf) ); + FD_TEST( rbuf->hi >= buf && rbuf->hi < buf+sizeof(buf) ); + FD_TEST( rbuf->lo_off <= rbuf->hi_off ); + + ulong action = fd_rng_ulong( rng ); + ulong free_sz = fd_h2_rbuf_free_sz( rbuf ); + ulong used_sz = fd_h2_rbuf_used_sz( rbuf ); + FD_TEST( used_sz==shadow_prod-shadow_cons ); + if( action & 1 ) { + /* push */ + ulong push_sz = fd_rng_ulong_roll( rng, free_sz+1UL ); + for( ulong j=0UL; j0 : iov_cnt==0 ); + FD_TEST( iov_cnt<=2 ); + if( iov_cnt ) { + FD_TEST( iov[0].iov_len+iov[1].iov_len==free_sz ); + ulong copy0_sz = fd_ulong_min( iov[0].iov_len, push_sz ); + fd_memcpy( iov[0].iov_base, scratch, copy0_sz ); + ulong copy1_sz = fd_ulong_min( iov[1].iov_len, push_sz-copy0_sz ); + if( copy1_sz ) fd_memcpy( iov[1].iov_base, scratch+copy0_sz, copy1_sz ); + fd_h2_rbuf_commit_recvmsg( rbuf, iov, push_sz ); + } + } + FD_TEST( fd_h2_rbuf_free_sz( rbuf )==free_sz-push_sz ); + FD_TEST( fd_h2_rbuf_used_sz( rbuf )==used_sz+push_sz ); + } else { + /* pop */ + ulong pop_sz = fd_rng_ulong_roll( rng, used_sz+1UL ); + if( action & (2+4+8) ) { + /* gather */ + ulong sz0, sz1; + uchar * b = fd_h2_rbuf_peek_used( rbuf, &sz0, &sz1 ); + for( ulong j=0UL; jbuf0; + } + } + fd_h2_rbuf_skip( rbuf, pop_sz ); + } else { + /* pop */ + uchar scratch2[64]; + uchar * b = fd_h2_rbuf_pop( rbuf, scratch2, pop_sz ); + for( ulong j=0UL; jbufsz ); + FD_TEST( fd_h2_rbuf_used_sz( rbuf )<=rbuf->bufsz ); + FD_TEST( fd_h2_rbuf_used_sz( rbuf )+fd_h2_rbuf_free_sz( rbuf )<=64 ); + FD_TEST( shadow_cons==rbuf->lo_off ); + } +} diff --git a/src/waltz/h2/test_h2_server.c b/src/waltz/h2/test_h2_server.c new file mode 100644 index 0000000000..128d2d2205 --- /dev/null +++ b/src/waltz/h2/test_h2_server.c @@ -0,0 +1,327 @@ +/* test_h2_server is a dummy HTTP/2 server for testing purposes. + It responds to GET and POST requests with status 200. + The server uses single threaded blocking sockets without timeouts. */ + +#include "fd_h2.h" +#include "fd_h2_rbuf_sock.h" +#include "../../util/fd_util.h" +#include "../../util/net/fd_ip4.h" + +#include +#include /* exit(2) */ +#include /* close(2) */ +#include /* IPPROTO_TCP */ +#include /* socket(2) */ + +/* App logic */ + +struct test_h2_app { + fd_h2_rbuf_t rbuf_tx[1]; + fd_h2_conn_t conn[1]; + fd_h2_stream_t stream[1]; + fd_h2_tx_op_t tx_op[1]; +}; + +typedef struct test_h2_app test_h2_app_t; + +static test_h2_app_t * g_app; + +static fd_h2_callbacks_t test_h2_callbacks; + +static void +test_response_continue( void ) { + fd_h2_stream_t * stream = g_app->stream; + if( !stream->stream_id ) return; + fd_h2_tx_op_copy( g_app->conn, stream, g_app->rbuf_tx, g_app->tx_op ); + if( stream->state==FD_H2_STREAM_STATE_CLOSED ) { + FD_LOG_NOTICE(( "Request %u: Response complete", stream->stream_id )); + memset( g_app->tx_op, 0, sizeof(g_app->tx_op ) ); + memset( g_app->stream, 0, sizeof(g_app->stream) ); + } +} + +static void +test_response_init( fd_h2_conn_t * conn, + fd_h2_stream_t * stream ) { + (void)conn; + uint stream_id = stream->stream_id; + FD_LOG_NOTICE(( "Request %u: Done reading, sending response", stream_id )); + + fd_h2_rbuf_t * rbuf_tx = g_app->rbuf_tx; + uchar hpack[] = { 0x88 /* :status: 200 */ }; + fd_h2_tx( rbuf_tx, hpack, sizeof(hpack), FD_H2_FRAME_TYPE_HEADERS, FD_H2_FLAG_END_HEADERS, stream_id ); + + fd_h2_tx_op_t * tx_op = g_app->tx_op; + fd_h2_tx_op_init( tx_op, "Ok", 2UL, FD_H2_FLAG_END_STREAM ); + test_response_continue(); + if( stream->state!=FD_H2_STREAM_STATE_CLOSED ) { + FD_LOG_NOTICE(( "Request %u: Blocked trying to write response, wrote %lu bytes", stream_id, 2UL - tx_op->chunk_sz )); + } else { + FD_LOG_NOTICE(( "Request %u: Response complete", stream_id )); + } +} + +/* fd_h2 callbacks */ + +static fd_h2_stream_t * +test_cb_stream_create( fd_h2_conn_t * conn, + uint stream_id ) { + (void)conn; + fd_h2_stream_t * stream = g_app->stream; + if( FD_UNLIKELY( stream->stream_id ) ) return NULL; + fd_h2_stream_open( fd_h2_stream_init( stream ), conn, stream_id ); + return stream; +} + +static fd_h2_stream_t * +test_cb_stream_query( fd_h2_conn_t * conn, + uint stream_id ) { + (void)conn; + fd_h2_stream_t * stream = g_app->stream; + if( stream->stream_id!=stream_id ) return NULL; + return stream; +} + +static void +test_cb_conn_established( fd_h2_conn_t * conn ) { + (void)conn; + FD_LOG_NOTICE(( "HTTP/2 conn established" )); +} + +static void +test_cb_conn_final( fd_h2_conn_t * conn, + uint h2_err, + int closed_by ) { + (void)conn; (void)closed_by; + FD_LOG_NOTICE(( "HTTP/2 conn closed (%u-%s)", h2_err, fd_h2_strerror( h2_err ) )); +} + +static void +test_cb_rst_stream( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + uint error_code, + int closed_by ) { + (void)conn; + FD_LOG_NOTICE(( "Request %u: %s RST_STREAM (%u-%s)", + stream->stream_id, + closed_by ? "received" : "sending", + error_code, fd_h2_strerror( error_code ) )); + memset( g_app->tx_op, 0, sizeof(fd_h2_tx_op_t) ); +} + +static void +test_cb_window_update( fd_h2_conn_t * conn, + uint delta ) { + (void)conn; (void)delta; + test_response_continue(); +} + +static void +test_cb_stream_window_update( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + uint delta ) { + (void)conn; (void)stream; (void)delta; + test_response_continue(); +} + +static void +test_cb_headers( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + void const * data, + ulong data_sz, + ulong flags ) { + FD_LOG_HEXDUMP_DEBUG(( "Header field block", data, data_sz )); + + fd_hpack_rd_t hpack_rd[1]; + fd_hpack_rd_init( hpack_rd, data, data_sz ); + while( !fd_hpack_rd_done( hpack_rd ) ) { + static uchar scratch_buf[ 4096 ]; + uchar * scratch = scratch_buf; + fd_h2_hdr_t hdr[1]; + uint err = fd_hpack_rd_next( hpack_rd, hdr, &scratch, scratch_buf+sizeof(scratch_buf) ); + if( FD_UNLIKELY( err ) ) { + FD_LOG_WARNING(( "Error reading headers (%u-%s)", err, fd_h2_strerror( err ) )); + fd_h2_conn_error( conn, err ); + return; + } + + FD_LOG_NOTICE(( "-> %.*s: %.*s", + (int)hdr->name_len, (char const *)hdr->name, + (int)hdr->value_len, (char const *)hdr->value )); + } + + if( flags & FD_H2_FLAG_END_HEADERS ) { + FD_LOG_NOTICE(( "Request %u: Headers complete", stream->stream_id )); + } + if( flags & FD_H2_FLAG_END_STREAM ) { + test_response_init( conn, stream ); + } +} + +static void +test_cb_data( fd_h2_conn_t * conn, + fd_h2_stream_t * stream, + void const * data, + ulong data_sz, + ulong flags ) { + if( FD_UNLIKELY( stream->state==FD_H2_STREAM_STATE_ILLEGAL ) ) { + fd_h2_conn_error( conn, FD_H2_ERR_PROTOCOL ); + return; + } + + FD_LOG_HEXDUMP_DEBUG(( "Request data", data, data_sz )); + + if( flags & FD_H2_FLAG_END_STREAM ) { + test_response_init( conn, stream ); + } +} + +static int +read_preface( int tcp_sock ) { + ulong preface_sz = 0UL; + uchar preface[ 24 ]; + + do { + long res = read( tcp_sock, preface+preface_sz, sizeof(preface)-preface_sz ); + if( FD_UNLIKELY( res<0L ) ) { + FD_LOG_WARNING(( "Failed to read client preface (%i-%s)", errno, fd_io_strerror( errno ) )); + return 0; + } + if( FD_UNLIKELY( res==0L ) ) { + FD_LOG_WARNING(( "Client closed connection before sending preface" )); + return 0; + } + if( FD_UNLIKELY( !fd_memeq( preface+preface_sz, fd_h2_client_preface+preface_sz, (ulong)res ) ) ) { + FD_LOG_WARNING(( "Not a HTTP/2 client" )); + return 0; + } + preface_sz += (ulong)res; + } while( preface_sz<24 ); + + return 1; +} + +static void +handle_conn( int tcp_sock ) { + test_h2_app_t app[1] = {0}; + static uchar scratch[ 16384 ]; + static uchar rx_buf [ 16384 ]; + static uchar tx_buf [ 16384 ]; + + if( FD_UNLIKELY( !read_preface( tcp_sock ) ) ) return; + + g_app = app; + fd_h2_conn_t * conn = g_app->conn; + fd_h2_conn_init_server( conn ); + conn->self_settings.max_concurrent_streams = 1; + + fd_h2_rbuf_t rbuf_rx[1]; + fd_h2_rbuf_t * rbuf_tx = g_app->rbuf_tx; + fd_h2_rbuf_init( rbuf_rx, rx_buf, sizeof(rx_buf) ); + fd_h2_rbuf_init( rbuf_tx, tx_buf, sizeof(tx_buf) ); + + for(;;) { + fd_h2_tx_control( conn, rbuf_tx, &test_h2_callbacks ); + + while( fd_h2_rbuf_used_sz( rbuf_tx ) ) { + int err = fd_h2_rbuf_sendmsg( rbuf_tx, tcp_sock, MSG_NOSIGNAL ); + if( FD_UNLIKELY( err ) ) { + FD_LOG_WARNING(( "sendmsg failed (%i-%s)", err, fd_io_strerror( err ) )); + return; + } + } + + if( FD_UNLIKELY( conn->flags & FD_H2_CONN_FLAGS_DEAD ) ) { + FD_LOG_NOTICE(( "Closing TCP conn" )); + return; + } + + int err = fd_h2_rbuf_recvmsg( rbuf_rx, tcp_sock, MSG_NOSIGNAL ); + if( FD_UNLIKELY( err==EPIPE ) ) { + FD_LOG_NOTICE(( "Peer closed TCP connection" )); + return; + } + if( FD_UNLIKELY( err ) ) { + FD_LOG_WARNING(( "recvmsg failed (%i-%s)", err, fd_io_strerror( err ) )); + return; + } + + fd_h2_rx( conn, rbuf_rx, rbuf_tx, scratch, sizeof(scratch), &test_h2_callbacks ); + } +} + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + fd_h2_callbacks_init( &test_h2_callbacks ); + test_h2_callbacks.stream_create = test_cb_stream_create; + test_h2_callbacks.stream_query = test_cb_stream_query; + test_h2_callbacks.conn_established = test_cb_conn_established; + test_h2_callbacks.conn_final = test_cb_conn_final; + test_h2_callbacks.headers = test_cb_headers; + test_h2_callbacks.data = test_cb_data; + test_h2_callbacks.rst_stream = test_cb_rst_stream; + test_h2_callbacks.window_update = test_cb_window_update; + test_h2_callbacks.stream_window_update = test_cb_stream_window_update; + + char const * bind_cstr = fd_env_strip_cmdline_cstr ( &argc, &argv, "--bind", NULL, "0.0.0.0" ); + ushort port = fd_env_strip_cmdline_ushort( &argc, &argv, "--port", NULL, 8080 ); + char const * mode = fd_env_strip_cmdline_cstr ( &argc, &argv, "--mode", NULL, "simple" ); + + int do_fork = 0; + if( !strcmp( mode, "simple" ) ) { + do_fork = 0; + } else if( !strcmp( mode, "fork" ) ) { + do_fork = 1; + } else { + FD_LOG_ERR(( "Unknown --mode '%s'", mode )); + } + + int listen_sock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP ); + if( FD_UNLIKELY( listen_sock<0 ) ) FD_LOG_ERR(( "socket(AF_INET,SOCK_STREAM,IPPROTO_TCP) failed (%i-%s)", errno, fd_io_strerror( errno ) )); + + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = fd_ushort_bswap( port ); + if( FD_UNLIKELY( !fd_cstr_to_ip4_addr( bind_cstr, &addr.sin_addr.s_addr ) ) ) { + FD_LOG_ERR(( "invalid --bind address" )); + } + if( FD_UNLIKELY( 0!=bind( listen_sock, fd_type_pun_const( &addr ), sizeof(struct sockaddr_in) ) ) ) { + FD_LOG_ERR(( "bind(:%hu) failed (%i-%s)", port, errno, fd_io_strerror( errno ) )); + } + + if( FD_UNLIKELY( 0!=listen( listen_sock, 2 ) ) ) { + FD_LOG_ERR(( "listen(listen_sock,1) failed (%i-%s)", errno, fd_io_strerror( errno ) )); + } + + FD_LOG_NOTICE(( "Listening at :%hu", port )); + + for(;;) { + struct sockaddr_storage peer_addr; + socklen_t peer_addr_sz = sizeof(struct sockaddr_storage); + int tcp_sock = accept( listen_sock, fd_type_pun( &peer_addr ), &peer_addr_sz ); + if( FD_UNLIKELY( tcp_sock<0 ) ) FD_LOG_ERR(( "accept(listen_sock) failed (%i-%s)", errno, fd_io_strerror( errno ) )); + + FD_LOG_NOTICE(( "Accepted TCP conn" )); + if( do_fork ) { + pid_t pid = fork(); + if( FD_UNLIKELY( pid<0 ) ) FD_LOG_ERR(( "fork() failed (%i-%s)", errno, fd_io_strerror( errno ) )); + if( pid==0 ) { + fd_log_private_tid_set( (ulong)getpid() ); + handle_conn( tcp_sock ); + exit( 0 ); + } + } else { + handle_conn( tcp_sock ); + } + + if( FD_UNLIKELY( 0!=close( tcp_sock ) ) ) FD_LOG_ERR(( "close(tcp_sock) failed (%i-%s)", errno, fd_io_strerror( errno ) )); + } + + if( FD_UNLIKELY( 0!=close( listen_sock ) ) ) FD_LOG_ERR(( "close(listen_sock) failed (%i-%s)", errno, fd_io_strerror( errno ) )); + + fd_halt(); + return 0; +} diff --git a/src/waltz/h2/test_hpack.c b/src/waltz/h2/test_hpack.c new file mode 100644 index 0000000000..2bf70b3ed1 --- /dev/null +++ b/src/waltz/h2/test_hpack.c @@ -0,0 +1,204 @@ +#include "fd_hpack_private.h" +#include "fd_hpack_wr.h" +#include "../../util/log/fd_log.h" + +static uchar const rfc7541_c31_bin[] = { + 0x82, 0x86, 0x84, 0x41, 0x0f, 0x77, 0x77, 0x77, + 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, + 0x2e, 0x63, 0x6f, 0x6d +}; + +static fd_h2_hdr_t const rfc7541_c31_dec[] = { + { .name =":method", .name_len =7, .hint=2 | FD_H2_HDR_HINT_INDEXED, + .value="GET", .value_len=3 }, + { .name =":scheme", .name_len =7, .hint=6 | FD_H2_HDR_HINT_INDEXED, + .value="http", .value_len=4 }, + { .name =":path", .name_len =5, .hint=4 | FD_H2_HDR_HINT_INDEXED, + .value="/", .value_len=1 }, + { .name =":authority", .name_len =10, .hint=1 | FD_H2_HDR_HINT_NAME_INDEXED, + .value="www.example.com", .value_len=15 }, + {0} +}; + +static uchar const rfc7541_c32_bin[] = { + 0x82, 0x86, 0x84, /* 0xbe, */ 0x58, 0x08, 0x6e, 0x6f, + 0x2d, 0x63, 0x61, 0x63, 0x68, 0x65 +}; + +static fd_h2_hdr_t const rfc7541_c32_dec[] = { + { .name =":method", .name_len =7, .hint=2 | FD_H2_HDR_HINT_INDEXED, + .value="GET", .value_len=3 }, + { .name =":scheme", .name_len =7, .hint=6 | FD_H2_HDR_HINT_INDEXED, + .value="http", .value_len=4 }, + { .name =":path", .name_len =5, .hint=4 | FD_H2_HDR_HINT_INDEXED, + .value="/", .value_len=1 }, + // FIXME removed dynamic table entry + //{ .name =":authority", .name_len =10, .hint=1 | FD_H2_HDR_HINT_NAME_INDEXED, + // .value="www.example.com", .value_len=15 }, + { .name ="cache-control", .name_len =13, .hint=24 | FD_H2_HDR_HINT_NAME_INDEXED, + .value="no-cache", .value_len=8 }, + {0} +}; + +static uchar const rfc7541_c33_bin[] = { + 0x82, 0x87, 0x85, /* 0xbf, */ 0x40, 0x0a, 0x63, 0x75, + 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x6b, 0x65, 0x79, + 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, + 0x76, 0x61, 0x6c, 0x75, 0x65 +}; + +static fd_h2_hdr_t const rfc7541_c33_dec[] = { + { .name =":method", .name_len =7, .hint=2 | FD_H2_HDR_HINT_INDEXED, + .value="GET", .value_len=3 }, + { .name =":scheme", .name_len =7, .hint=7 | FD_H2_HDR_HINT_INDEXED, + .value="https", .value_len=5 }, + { .name =":path", .name_len =5, .hint=5 | FD_H2_HDR_HINT_INDEXED, + .value="/index.html", .value_len=11 }, + // FIXME removed dynamic table entry + //{ .name =":authority", .name_len =10, .hint=1 | FD_H2_HDR_HINT_NAME_INDEXED, + // .value="www.example.com", .value_len=15 }, + { .name ="custom-key", .name_len =10, + .value="custom-value", .value_len=12 }, + {0} +}; + +static uchar const rfc7541_c41_bin[] = { + 0x82, 0x86, 0x84, 0x41, 0x8c, 0xf1, 0xe3, 0xc2, + 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, + 0xff +}; + +static uchar const rfc7541_c42_bin[] = { + 0x82, 0x86, 0x84, /* 0xbe, */ 0x58, 0x86, 0xa8, 0xeb, + 0x10, 0x64, 0x9c, 0xbf +}; + +static uchar const rfc7541_c43_bin[] = { + 0x82, 0x87, 0x85, /* 0xbf, */ 0x40, 0x88, 0x25, 0xa8, + 0x49, 0xe9, 0x5b, 0xa9, 0x7d, 0x7f, 0x89, 0x25, + 0xa8, 0x49, 0xe9, 0x5b, 0xb8, 0xe8, 0xb4, 0xbf +}; + +static uchar const rfc7541_c51_bin[] = { + 0x48, 0x03, 0x33, 0x30, 0x32, 0x58, 0x07, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x61, 0x1d, + 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x31, 0x20, + 0x4f, 0x63, 0x74, 0x20, 0x32, 0x30, 0x31, 0x33, + 0x20, 0x32, 0x30, 0x3a, 0x31, 0x33, 0x3a, 0x32, + 0x31, 0x20, 0x47, 0x4d, 0x54, 0x6e, 0x17, 0x68, + 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x77, + 0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d +}; + +static fd_h2_hdr_t const rfc7541_c51_dec[] = { + { .name=":status", .name_len = 7, .hint=8 | FD_H2_HDR_HINT_NAME_INDEXED, + .value="302", .value_len= 3 }, + { .name="cache-control", .name_len =13, .hint=24 | FD_H2_HDR_HINT_NAME_INDEXED, + .value="private", .value_len= 7 }, + { .name="date", .name_len = 4, .hint=33 | FD_H2_HDR_HINT_NAME_INDEXED, + .value="Mon, 21 Oct 2013 20:13:21 GMT", .value_len=29 }, + { .name="location", .name_len = 8, .hint=46 | FD_H2_HDR_HINT_NAME_INDEXED, + .value="https://www.example.com", .value_len=23 }, + {0} +}; + +static uchar const rfc7541_c61_bin[] = { + 0x48, 0x82, 0x64, 0x02, 0x58, 0x85, 0xae, 0xc3, + 0x77, 0x1a, 0x4b, 0x61, 0x96, 0xd0, 0x7a, 0xbe, + 0x94, 0x10, 0x54, 0xd4, 0x44, 0xa8, 0x20, 0x05, + 0x95, 0x04, 0x0b, 0x81, 0x66, 0xe0, 0x82, 0xa6, + 0x2d, 0x1b, 0xff, 0x6e, 0x91, 0x9d, 0x29, 0xad, + 0x17, 0x18, 0x63, 0xc7, 0x8f, 0x0b, 0x97, 0xc8, + 0xe9, 0xae, 0x82, 0xae, 0x43, 0xd3 +}; + +static void +test_hpack_rd( uchar const * bin, + ulong binsz, + fd_h2_hdr_t const * dec ) { + fd_hpack_rd_t rd[1]; + fd_hpack_rd_init( rd, bin, binsz ); + for( fd_h2_hdr_t const * expected=dec; expected->name; expected++ ) { + FD_TEST( !fd_hpack_rd_done( rd ) ); + fd_h2_hdr_t hdr[1]; + uchar buf[ 128 ]; + uchar * bufp = buf; + FD_TEST( fd_hpack_rd_next( rd, hdr, &bufp, buf+sizeof(buf) )==FD_H2_SUCCESS ); + FD_TEST( bufp>=buf && bufp<=buf+sizeof(buf) ); + FD_TEST( hdr->name_len == expected->name_len ); + FD_TEST( hdr->value_len == expected->value_len ); + FD_TEST( fd_memeq( hdr->name, expected->name, expected->name_len ) ); + FD_TEST( fd_memeq( hdr->value, expected->value, expected->value_len ) ); + FD_TEST( hdr->hint == expected->hint ); + } + FD_TEST( fd_hpack_rd_done( rd ) ); +} + +struct test_hpack_case { + ulong res; + uchar enc[ 8 ]; + uchar bits; + uchar prefix; + uchar len; +}; + +typedef struct test_hpack_case test_hpack_case_t; + +static test_hpack_case_t const test_hpack_cases[] = { + { .bits=1, .prefix=0x00, .len=0, .res= 0UL }, + { .bits=2, .prefix=0x02, .len=0, .res= 2UL }, + { .bits=3, .prefix=0x06, .len=0, .res= 6UL }, + { .bits=4, .prefix=0x0e, .len=0, .res= 14UL }, + { .bits=5, .prefix=0x1e, .len=0, .res= 30UL }, + { .bits=6, .prefix=0x3e, .len=0, .res= 62UL }, + { .bits=7, .prefix=0x7e, .len=0, .res= 126UL }, + { .bits=8, .prefix=0xfe, .len=0, .res= 254UL }, + { .bits=5, .prefix=0xff, .len=2, .res=1337UL, .enc={0x9a, 0x0a} }, + { .bits=5, .prefix=0x9f, .len=2, .res=1337UL, .enc={0x9a, 0x0a} }, + { .bits=5, .prefix=0xbf, .len=2, .res=1337UL, .enc={0x9a, 0x0a} }, + { .bits=7, .prefix=0x7f, .len=1, .res= 179UL, .enc={0x34} }, + { .bits=0 } +}; + +static void +test_hpack_rd_varint( void ) { + for( test_hpack_case_t const * c=test_hpack_cases; c->bits; c++ ) { + for( ulong len=0UL; len<=8UL; len++ ) { + fd_hpack_rd_t rd = { .src=c->enc, .src_end=c->enc+len }; + ulong res = fd_hpack_rd_varint( &rd, c->prefix, (1U<<(c->bits))-1U ); + if( len < c->len ) { + FD_TEST( res==ULONG_MAX ); + } else { + FD_TEST( res==c->res ); + } + } + } +} + +static void +test_hpack_wr_varint( void ) { + for( test_hpack_case_t const * c=test_hpack_cases; c->bits; c++ ) { + uchar buf[ 16 ]; + uint addend = (1U<<(c->bits))-1U; + uint prefix = c->prefix & ~addend; + ulong len = fd_hpack_wr_varint( buf, prefix, addend, c->res ); + FD_TEST( len == (ulong)c->len+1 ); + FD_TEST( buf[0] == c->prefix ); + FD_TEST( fd_memeq( buf+1, c->enc, c->len ) ); + } +} + +void +test_hpack( void ) { + test_hpack_rd( rfc7541_c31_bin, sizeof(rfc7541_c31_bin), rfc7541_c31_dec ); + test_hpack_rd( rfc7541_c32_bin, sizeof(rfc7541_c32_bin), rfc7541_c32_dec ); + test_hpack_rd( rfc7541_c33_bin, sizeof(rfc7541_c33_bin), rfc7541_c33_dec ); + test_hpack_rd( rfc7541_c41_bin, sizeof(rfc7541_c41_bin), rfc7541_c31_dec ); + test_hpack_rd( rfc7541_c42_bin, sizeof(rfc7541_c42_bin), rfc7541_c32_dec ); + test_hpack_rd( rfc7541_c43_bin, sizeof(rfc7541_c43_bin), rfc7541_c33_dec ); + test_hpack_rd( rfc7541_c51_bin, sizeof(rfc7541_c51_bin), rfc7541_c51_dec ); + test_hpack_rd( rfc7541_c61_bin, sizeof(rfc7541_c61_bin), rfc7541_c51_dec ); + test_hpack_rd_varint(); + test_hpack_wr_varint(); +} diff --git a/src/waltz/http/Local.mk b/src/waltz/http/Local.mk index a1ab72efaf..a01e28e25d 100644 --- a/src/waltz/http/Local.mk +++ b/src/waltz/http/Local.mk @@ -20,3 +20,7 @@ ifdef FD_HAS_HOSTED $(call make-fuzz-test,fuzz_picohttpparser,fuzz_picohttpparser,fd_waltz fd_util) $(call make-fuzz-test,fuzz_httpserver,fuzz_httpserver,fd_waltz fd_ballet fd_util) endif + +$(call add-hdrs,fd_url.h) +$(call add-objs,fd_url,fd_waltz) +$(call make-fuzz-test,fuzz_url_parse,fuzz_url_parse,fd_waltz fd_util) diff --git a/src/waltz/http/fd_url.c b/src/waltz/http/fd_url.c new file mode 100644 index 0000000000..2325795836 --- /dev/null +++ b/src/waltz/http/fd_url.c @@ -0,0 +1,72 @@ +#include "fd_url.h" + +fd_url_t * +fd_url_parse_cstr( fd_url_t * const url, + char const * const url_str, + ulong const url_str_len, + int * opt_err ) { + int err_[1]; + if( !opt_err ) opt_err = err_; + *opt_err = FD_URL_SUCCESS; + + char const * const url_end = url_str+url_str_len; + + char const * const scheme = url_str; + ulong scheme_len = 0UL; + if( FD_UNLIKELY( url_str_len<8UL ) ) return NULL; + if( fd_memeq( scheme, "http://", 7 ) ) { + scheme_len = 7; + } else if( fd_memeq( scheme, "https://", 8 ) ) { + scheme_len = 8; + } else { + *opt_err = FD_URL_ERR_SCHEME; + return NULL; + } + + char const * const authority = scheme+scheme_len; + + /* Find beginning of path */ + char const * authority_end; + for( authority_end = authority; + authority_end < url_end && *authority_end!='/'; + authority_end++ ) { + if( FD_UNLIKELY( *authority_end=='@' ) ) { + *opt_err = FD_URL_ERR_USERINFO; + return NULL; /* userinfo not supported */ + } + } + ulong const authority_len = (ulong)( authority_end-authority ); + + /* Find port number */ + char const * const host = authority; + ulong host_len = authority_len; + char const * port = NULL; + ulong port_len = 0UL; + for( ulong j=0UL; j255 ) ) { + *opt_err = FD_URL_ERR_HOST_OVERSZ; + return NULL; + } + + + *url = (fd_url_t){ + .scheme = scheme, + .scheme_len = scheme_len, + .host = host, + .host_len = host_len, + .port = port, + .port_len = port_len, + .tail = authority+authority_len, + .tail_len = (ulong)( url_end-(authority+authority_len) ) + }; + + return url; +} diff --git a/src/waltz/http/fd_url.h b/src/waltz/http/fd_url.h new file mode 100644 index 0000000000..b0c1c756c9 --- /dev/null +++ b/src/waltz/http/fd_url.h @@ -0,0 +1,52 @@ +#ifndef HEADER_fd_src_waltz_h2_fd_url_h +#define HEADER_fd_src_waltz_h2_fd_url_h + +/* fd_url.h provides an API for handling URLs. + + This API is by no means compliant. Works only for basic strings. */ + +#include "../../util/fd_util_base.h" + +/* fd_url_t holds a bunch of pointers into an URL string. */ + +struct fd_url { + char const * scheme; + ulong scheme_len; + + char const * host; + ulong host_len; /* <=255 */ + + char const * port; + ulong port_len; + + char const * tail; /* path, query, fragment */ + ulong tail_len; +}; + +typedef struct fd_url fd_url_t; + +#define FD_URL_SUCCESS 0 +#define FD_URL_ERR_SCHEME 1 +#define FD_URL_ERR_HOST_OVERSZ 2 +#define FD_URL_ERR_USERINFO 3 + +FD_PROTOTYPES_BEGIN + +/* fd_url_parse_cstr is a basic URL parser. It is not RFC compliant. + + Non-exhaustive list of what this function cannot do: + - Schemes other than http and https are not supported + - userinfo (e.g. 'user:pass@') is not supported + - Anything after the authority is ignored + + If opt_err!=NULL, on return *opt_err holds an FD_URL_ERR_{...} code. */ + +fd_url_t * +fd_url_parse_cstr( fd_url_t * url, + char const * url_str, + ulong url_str_len, + int * opt_err ); + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_waltz_h2_fd_url_h */ diff --git a/src/waltz/http/fuzz_url_parse.c b/src/waltz/http/fuzz_url_parse.c new file mode 100644 index 0000000000..e9e05d157d --- /dev/null +++ b/src/waltz/http/fuzz_url_parse.c @@ -0,0 +1,47 @@ +#if !FD_HAS_HOSTED +#error "This target requires FD_HAS_HOSTED" +#endif + +#include +#include + +#include "fd_url.h" +#include "../../util/fd_util.h" + +int +LLVMFuzzerInitialize( int * argc, + char *** argv ) { + /* Set up shell without signal handlers */ + putenv( "FD_LOG_BACKTRACE=0" ); + putenv( "FD_LOG_PATH=" ); + fd_boot( argc, argv ); + fd_log_level_core_set(0); /* crash on debug log */ + return 0; +} + +static void +bounds_check( uchar const * c0, + ulong s0, + char const * c1_, + ulong s1 ) { + if( !s1 ) return; + uchar const * c1 = (uchar const *)c1_; + assert( s1<=s0 ); + assert( c1>=c0 ); + assert( c1scheme, url->scheme_len ); + bounds_check( data, size, url->host, url->host_len ); + bounds_check( data, size, url->port, url->port_len ); + bounds_check( data, size, url->tail, url->tail_len ); + } + return 0; +} diff --git a/src/waltz/openssl/Local.mk b/src/waltz/openssl/Local.mk new file mode 100644 index 0000000000..0bf978e681 --- /dev/null +++ b/src/waltz/openssl/Local.mk @@ -0,0 +1,4 @@ +$(call add-hdrs,fd_openssl.h) +ifdef FD_HAS_OPENSSL +$(call add-objs,fd_openssl,fd_waltz) +endif diff --git a/src/waltz/openssl/fd_openssl.c b/src/waltz/openssl/fd_openssl.c new file mode 100644 index 0000000000..e7dff70983 --- /dev/null +++ b/src/waltz/openssl/fd_openssl.c @@ -0,0 +1,27 @@ +#include "fd_openssl.h" + +#if !FD_HAS_OPENSSL +#error "fd_openssl.c requires FD_HAS_OPENSSL" +#endif + +#include + +FD_FN_CONST char const * +fd_openssl_ssl_strerror( int ssl_err ) { + switch( ssl_err ) { + case SSL_ERROR_NONE: return "SSL_ERROR_NONE"; + case SSL_ERROR_SSL: return "SSL_ERROR_SSL"; + case SSL_ERROR_WANT_READ: return "SSL_ERROR_WANT_READ"; + case SSL_ERROR_WANT_WRITE: return "SSL_ERROR_WANT_WRITE"; + case SSL_ERROR_WANT_X509_LOOKUP: return "SSL_ERROR_WANT_X509_LOOKUP"; + case SSL_ERROR_SYSCALL: return "SSL_ERROR_SYSCALL"; + case SSL_ERROR_ZERO_RETURN: return "SSL_ERROR_ZERO_RETURN"; + case SSL_ERROR_WANT_CONNECT: return "SSL_ERROR_WANT_CONNECT"; + case SSL_ERROR_WANT_ACCEPT: return "SSL_ERROR_WANT_ACCEPT"; + case SSL_ERROR_WANT_ASYNC: return "SSL_ERROR_WANT_ASYNC"; + case SSL_ERROR_WANT_ASYNC_JOB: return "SSL_ERROR_WANT_ASYNC_JOB"; + case SSL_ERROR_WANT_CLIENT_HELLO_CB: return "SSL_ERROR_WANT_CLIENT_HELLO_CB"; + case SSL_ERROR_WANT_RETRY_VERIFY: return "SSL_ERROR_WANT_RETRY_VERIFY"; + default: return "unknown"; + } +} diff --git a/src/waltz/openssl/fd_openssl.h b/src/waltz/openssl/fd_openssl.h new file mode 100644 index 0000000000..79b7c2ed2f --- /dev/null +++ b/src/waltz/openssl/fd_openssl.h @@ -0,0 +1,22 @@ +#ifndef HEADER_fd_src_waltz_openssl_fd_openssl_h +#define HEADER_fd_src_waltz_openssl_fd_openssl_h + +#include "../../util/fd_util_base.h" + +#if FD_HAS_OPENSSL + +FD_PROTOTYPES_BEGIN + +/* fd_openssl_ssl_strerror returns a human-readable string for SSL error + codes like SSL_ERROR_ZERO_RETURN. Unfortunately, no such strerror + API exists in OpenSSL itself, for APIs that don't append to the error + queue. */ + +FD_FN_CONST char const * +fd_openssl_ssl_strerror( int ssl_err ); + +FD_PROTOTYPES_END + +#endif /* FD_HAS_OPENSSL */ + +#endif /* HEADER_fd_src_waltz_openssl_fd_openssl_h */