diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /netwerk/socket/neqo_glue/src | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'netwerk/socket/neqo_glue/src')
-rw-r--r-- | netwerk/socket/neqo_glue/src/lib.rs | 1330 |
1 files changed, 1330 insertions, 0 deletions
diff --git a/netwerk/socket/neqo_glue/src/lib.rs b/netwerk/socket/neqo_glue/src/lib.rs new file mode 100644 index 0000000000..adc55042c5 --- /dev/null +++ b/netwerk/socket/neqo_glue/src/lib.rs @@ -0,0 +1,1330 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#[cfg(not(windows))] +use libc::{AF_INET, AF_INET6}; +use neqo_common::event::Provider; +use neqo_common::{self as common, qlog::NeqoQlog, qwarn, Datagram, Header, Role}; +use neqo_crypto::{init, PRErrorCode}; +use neqo_http3::{ + features::extended_connect::SessionCloseReason, Error as Http3Error, Http3Client, + Http3ClientEvent, Http3Parameters, Http3State, Priority, WebTransportEvent, +}; +use neqo_transport::{ + stream_id::StreamType, CongestionControlAlgorithm, ConnectionParameters, + Error as TransportError, Output, RandomConnectionIdGenerator, StreamId, Version, +}; +use nserror::*; +use nsstring::*; +use qlog::QlogStreamer; +use std::borrow::Cow; +use std::cell::RefCell; +use std::convert::TryFrom; +use std::convert::TryInto; +use std::fs::OpenOptions; +use std::net::SocketAddr; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::path::PathBuf; +use std::ptr; +use std::rc::Rc; +use std::slice; +use std::str; +#[cfg(feature = "fuzzing")] +use std::time::Duration; +use std::time::Instant; +use thin_vec::ThinVec; +#[cfg(windows)] +use winapi::shared::ws2def::{AF_INET, AF_INET6}; +use xpcom::{AtomicRefcnt, RefCounted, RefPtr}; + +#[repr(C)] +pub struct NeqoHttp3Conn { + conn: Http3Client, + local_addr: SocketAddr, + refcnt: AtomicRefcnt, +} + +// Opaque interface to mozilla::net::NetAddr defined in DNS.h +#[repr(C)] +pub union NetAddr { + private: [u8; 0], +} + +extern "C" { + pub fn moz_netaddr_get_family(arg: *const NetAddr) -> u16; + pub fn moz_netaddr_get_network_order_ip(arg: *const NetAddr) -> u32; + pub fn moz_netaddr_get_ipv6(arg: *const NetAddr) -> *const u8; + pub fn moz_netaddr_get_network_order_port(arg: *const NetAddr) -> u16; +} + +fn netaddr_to_socket_addr(arg: *const NetAddr) -> Result<SocketAddr, nsresult> { + if arg == ptr::null() { + return Err(NS_ERROR_INVALID_ARG); + } + + unsafe { + let family = moz_netaddr_get_family(arg) as i32; + if family == AF_INET { + let port = u16::from_be(moz_netaddr_get_network_order_port(arg)); + let ipv4 = Ipv4Addr::from(u32::from_be(moz_netaddr_get_network_order_ip(arg))); + return Ok(SocketAddr::new(IpAddr::V4(ipv4), port)); + } + + if family == AF_INET6 { + let port = u16::from_be(moz_netaddr_get_network_order_port(arg)); + let ipv6_slice: [u8; 16] = slice::from_raw_parts(moz_netaddr_get_ipv6(arg), 16) + .try_into() + .expect("slice with incorrect length"); + let ipv6 = Ipv6Addr::from(ipv6_slice); + return Ok(SocketAddr::new(IpAddr::V6(ipv6), port)); + } + } + + Err(NS_ERROR_UNEXPECTED) +} + +impl NeqoHttp3Conn { + fn new( + origin: &nsACString, + alpn: &nsACString, + local_addr: *const NetAddr, + remote_addr: *const NetAddr, + max_table_size: u64, + max_blocked_streams: u16, + max_data: u64, + max_stream_data: u64, + version_negotiation: bool, + webtransport: bool, + qlog_dir: &nsACString, + webtransport_datagram_size: u32, + ) -> Result<RefPtr<NeqoHttp3Conn>, nsresult> { + // Nss init. + init(); + + let origin_conv = str::from_utf8(origin).map_err(|_| NS_ERROR_INVALID_ARG)?; + + let alpn_conv = str::from_utf8(alpn).map_err(|_| NS_ERROR_INVALID_ARG)?; + + let local: SocketAddr = netaddr_to_socket_addr(local_addr)?; + + let remote: SocketAddr = netaddr_to_socket_addr(remote_addr)?; + + let quic_version = match alpn_conv { + "h3-32" => Version::Draft32, + "h3-31" => Version::Draft31, + "h3-30" => Version::Draft30, + "h3-29" => Version::Draft29, + "h3" => Version::Version1, + _ => return Err(NS_ERROR_INVALID_ARG), + }; + + let version_list = if version_negotiation { + Version::all() + } else { + vec![quic_version] + }; + #[allow(unused_mut)] + let mut params = ConnectionParameters::default() + .versions(quic_version, version_list) + .cc_algorithm(CongestionControlAlgorithm::Cubic) + .max_data(max_data) + .max_stream_data(StreamType::BiDi, false, max_stream_data); + + // Set a short timeout when fuzzing. + #[cfg(feature = "fuzzing")] + if static_prefs::pref!("fuzzing.necko.http3") { + params = params.idle_timeout(Duration::from_millis(10)); + } + + if webtransport_datagram_size > 0 { + params = params.datagram_size(webtransport_datagram_size.into()); + } + + let http3_settings = Http3Parameters::default() + .max_table_size_encoder(max_table_size) + .max_table_size_decoder(max_table_size) + .max_blocked_streams(max_blocked_streams) + .max_concurrent_push_streams(0) + .connection_parameters(params) + .webtransport(webtransport) + .http3_datagram(webtransport); + + let mut conn = match Http3Client::new( + origin_conv, + Rc::new(RefCell::new(RandomConnectionIdGenerator::new(3))), + local, + remote, + http3_settings, + Instant::now(), + ) { + Ok(c) => c, + Err(_) => return Err(NS_ERROR_INVALID_ARG), + }; + + if !qlog_dir.is_empty() { + let qlog_dir_conv = str::from_utf8(qlog_dir).map_err(|_| NS_ERROR_INVALID_ARG)?; + let mut qlog_path = PathBuf::from(qlog_dir_conv); + qlog_path.push(format!("{}.qlog", origin)); + + // Emit warnings but to not return an error if qlog initialization + // fails. + match OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&qlog_path) + { + Err(_) => qwarn!("Could not open qlog path: {}", qlog_path.display()), + Ok(f) => { + let streamer = QlogStreamer::new( + qlog::QLOG_VERSION.to_string(), + Some("Firefox Client qlog".to_string()), + Some("Firefox Client qlog".to_string()), + None, + std::time::Instant::now(), + common::qlog::new_trace(Role::Client), + Box::new(f), + ); + + match NeqoQlog::enabled(streamer, &qlog_path) { + Err(_) => qwarn!("Could not write to qlog path: {}", qlog_path.display()), + Ok(nq) => conn.set_qlog(nq), + } + } + } + } + + let conn = Box::into_raw(Box::new(NeqoHttp3Conn { + conn, + local_addr: local, + refcnt: unsafe { AtomicRefcnt::new() }, + })); + unsafe { Ok(RefPtr::from_raw(conn).unwrap()) } + } +} + +#[no_mangle] +pub unsafe extern "C" fn neqo_http3conn_addref(conn: &NeqoHttp3Conn) { + conn.refcnt.inc(); +} + +#[no_mangle] +pub unsafe extern "C" fn neqo_http3conn_release(conn: &NeqoHttp3Conn) { + let rc = conn.refcnt.dec(); + if rc == 0 { + std::mem::drop(Box::from_raw(conn as *const _ as *mut NeqoHttp3Conn)); + } +} + +// xpcom::RefPtr support +unsafe impl RefCounted for NeqoHttp3Conn { + unsafe fn addref(&self) { + neqo_http3conn_addref(self); + } + unsafe fn release(&self) { + neqo_http3conn_release(self); + } +} + +// Allocate a new NeqoHttp3Conn object. +#[no_mangle] +pub extern "C" fn neqo_http3conn_new( + origin: &nsACString, + alpn: &nsACString, + local_addr: *const NetAddr, + remote_addr: *const NetAddr, + max_table_size: u64, + max_blocked_streams: u16, + max_data: u64, + max_stream_data: u64, + version_negotiation: bool, + webtransport: bool, + qlog_dir: &nsACString, + webtransport_datagram_size: u32, + result: &mut *const NeqoHttp3Conn, +) -> nsresult { + *result = ptr::null_mut(); + + match NeqoHttp3Conn::new( + origin, + alpn, + local_addr, + remote_addr, + max_table_size, + max_blocked_streams, + max_data, + max_stream_data, + version_negotiation, + webtransport, + qlog_dir, + webtransport_datagram_size, + ) { + Ok(http3_conn) => { + http3_conn.forget(result); + NS_OK + } + Err(e) => e, + } +} + +/* Process a packet. + * packet holds packet data. + */ +#[no_mangle] +pub unsafe extern "C" fn neqo_http3conn_process_input( + conn: &mut NeqoHttp3Conn, + remote_addr: *const NetAddr, + packet: *const ThinVec<u8>, +) -> nsresult { + let remote = match netaddr_to_socket_addr(remote_addr) { + Ok(addr) => addr, + Err(result) => return result, + }; + conn.conn.process_input( + Datagram::new(remote, conn.local_addr, (*packet).to_vec()), + Instant::now(), + ); + return NS_OK; +} + +/* Process output: + * this may return a packet that needs to be sent or a timeout. + * if it returns a packet the function returns true, otherwise it returns false. + */ +#[no_mangle] +pub extern "C" fn neqo_http3conn_process_output( + conn: &mut NeqoHttp3Conn, + remote_addr: &mut nsACString, + remote_port: &mut u16, + packet: &mut ThinVec<u8>, + timeout: &mut u64, +) -> bool { + match conn.conn.process_output(Instant::now()) { + Output::Datagram(dg) => { + packet.extend_from_slice(&dg); + remote_addr.append(&dg.destination().ip().to_string()); + *remote_port = dg.destination().port(); + true + } + Output::Callback(to) => { + *timeout = to.as_millis() as u64; + // Necko resolution is in milliseconds whereas neqo resolution + // is in nanoseconds. If we called process_output too soon due + // to this difference, we might do few unnecessary loops until + // we waste the remaining time. To avoid it, we return 1ms when + // the timeout is less than 1ms. + if *timeout == 0 { + *timeout = 1; + } + false + } + Output::None => { + *timeout = std::u64::MAX; + false + } + } +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_close(conn: &mut NeqoHttp3Conn, error: u64) { + conn.conn.close(Instant::now(), error, ""); +} + +fn is_excluded_header(name: &str) -> bool { + if (name == "connection") + || (name == "host") + || (name == "keep-alive") + || (name == "proxy-connection") + || (name == "te") + || (name == "transfer-encoding") + || (name == "upgrade") + || (name == "sec-websocket-key") + { + true + } else { + false + } +} + +fn parse_headers(headers: &nsACString) -> Result<Vec<Header>, nsresult> { + let mut hdrs = Vec::new(); + // this is only used for headers built by Firefox. + // Firefox supplies all headers already prepared for sending over http1. + // They need to be split into (String, String) pairs. + match str::from_utf8(headers) { + Err(_) => { + return Err(NS_ERROR_INVALID_ARG); + } + Ok(h) => { + for elem in h.split("\r\n").skip(1) { + if elem.starts_with(':') { + // colon headers are for http/2 and 3 and this is http/1 + // input, so that is probably a smuggling attack of some + // kind. + continue; + } + if elem.len() == 0 { + continue; + } + let hdr_str: Vec<_> = elem.splitn(2, ":").collect(); + let name = hdr_str[0].trim().to_lowercase(); + if is_excluded_header(&name) { + continue; + } + let value = if hdr_str.len() > 1 { + String::from(hdr_str[1].trim()) + } else { + String::new() + }; + + hdrs.push(Header::new(name, value)); + } + } + } + Ok(hdrs) +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_fetch( + conn: &mut NeqoHttp3Conn, + method: &nsACString, + scheme: &nsACString, + host: &nsACString, + path: &nsACString, + headers: &nsACString, + stream_id: &mut u64, + urgency: u8, + incremental: bool, +) -> nsresult { + let hdrs = match parse_headers(headers) { + Err(e) => { + return e; + } + Ok(h) => h, + }; + let method_tmp = match str::from_utf8(method) { + Ok(m) => m, + Err(_) => { + return NS_ERROR_INVALID_ARG; + } + }; + let scheme_tmp = match str::from_utf8(scheme) { + Ok(s) => s, + Err(_) => { + return NS_ERROR_INVALID_ARG; + } + }; + let host_tmp = match str::from_utf8(host) { + Ok(h) => h, + Err(_) => { + return NS_ERROR_INVALID_ARG; + } + }; + let path_tmp = match str::from_utf8(path) { + Ok(p) => p, + Err(_) => { + return NS_ERROR_INVALID_ARG; + } + }; + if urgency >= 8 { + return NS_ERROR_INVALID_ARG; + } + let priority = Priority::new(urgency, incremental); + match conn.conn.fetch( + Instant::now(), + method_tmp, + &(scheme_tmp, host_tmp, path_tmp), + &hdrs, + priority, + ) { + Ok(id) => { + *stream_id = id.as_u64(); + NS_OK + } + Err(Http3Error::StreamLimitError) => NS_BASE_STREAM_WOULD_BLOCK, + Err(_) => NS_ERROR_UNEXPECTED, + } +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_priority_update( + conn: &mut NeqoHttp3Conn, + stream_id: u64, + urgency: u8, + incremental: bool, +) -> nsresult { + if urgency >= 8 { + return NS_ERROR_INVALID_ARG; + } + let priority = Priority::new(urgency, incremental); + match conn + .conn + .priority_update(StreamId::from(stream_id), priority) + { + Ok(_) => NS_OK, + Err(_) => NS_ERROR_UNEXPECTED, + } +} + +#[no_mangle] +pub unsafe extern "C" fn neqo_htttp3conn_send_request_body( + conn: &mut NeqoHttp3Conn, + stream_id: u64, + buf: *const u8, + len: u32, + read: &mut u32, +) -> nsresult { + let array = slice::from_raw_parts(buf, len as usize); + match conn.conn.send_data(StreamId::from(stream_id), array) { + Ok(amount) => { + *read = u32::try_from(amount).unwrap(); + if amount == 0 { + NS_BASE_STREAM_WOULD_BLOCK + } else { + NS_OK + } + } + Err(_) => NS_ERROR_UNEXPECTED, + } +} + +fn crypto_error_code(err: neqo_crypto::Error) -> u64 { + match err { + neqo_crypto::Error::AeadError => 1, + neqo_crypto::Error::CertificateLoading => 2, + neqo_crypto::Error::CreateSslSocket => 3, + neqo_crypto::Error::HkdfError => 4, + neqo_crypto::Error::InternalError => 5, + neqo_crypto::Error::IntegerOverflow => 6, + neqo_crypto::Error::InvalidEpoch => 7, + neqo_crypto::Error::MixedHandshakeMethod => 8, + neqo_crypto::Error::NoDataAvailable => 9, + neqo_crypto::Error::NssError { .. } => 10, + neqo_crypto::Error::OverrunError => 11, + neqo_crypto::Error::SelfEncryptFailure => 12, + neqo_crypto::Error::TimeTravelError => 13, + neqo_crypto::Error::UnsupportedCipher => 14, + neqo_crypto::Error::UnsupportedVersion => 15, + neqo_crypto::Error::StringError => 16, + neqo_crypto::Error::EchRetry(_) => 17, + neqo_crypto::Error::CipherInitFailure => 18, + } +} + +// This is only used for telemetry. Therefore we only return error code +// numbers and do not label them. Recording telemetry is easier with a +// number. +#[repr(C)] +pub enum CloseError { + TransportInternalError(u16), + TransportInternalErrorOther(u16), + TransportError(u64), + CryptoError(u64), + CryptoAlert(u8), + PeerAppError(u64), + PeerError(u64), + AppError(u64), + EchRetry, +} + +impl From<TransportError> for CloseError { + fn from(error: TransportError) -> CloseError { + match error { + TransportError::InternalError(c) => CloseError::TransportInternalError(c), + TransportError::CryptoError(neqo_crypto::Error::EchRetry(_)) => CloseError::EchRetry, + TransportError::CryptoError(c) => CloseError::CryptoError(crypto_error_code(c)), + TransportError::CryptoAlert(c) => CloseError::CryptoAlert(c), + TransportError::PeerApplicationError(c) => CloseError::PeerAppError(c), + TransportError::PeerError(c) => CloseError::PeerError(c), + TransportError::NoError + | TransportError::IdleTimeout + | TransportError::ConnectionRefused + | TransportError::FlowControlError + | TransportError::StreamLimitError + | TransportError::StreamStateError + | TransportError::FinalSizeError + | TransportError::FrameEncodingError + | TransportError::TransportParameterError + | TransportError::ProtocolViolation + | TransportError::InvalidToken + | TransportError::KeysExhausted + | TransportError::ApplicationError + | TransportError::NoAvailablePath => CloseError::TransportError(error.code()), + TransportError::EchRetry(_) => CloseError::EchRetry, + TransportError::AckedUnsentPacket => CloseError::TransportInternalErrorOther(0), + TransportError::ConnectionIdLimitExceeded => CloseError::TransportInternalErrorOther(1), + TransportError::ConnectionIdsExhausted => CloseError::TransportInternalErrorOther(2), + TransportError::ConnectionState => CloseError::TransportInternalErrorOther(3), + TransportError::DecodingFrame => CloseError::TransportInternalErrorOther(4), + TransportError::DecryptError => CloseError::TransportInternalErrorOther(5), + TransportError::HandshakeFailed => CloseError::TransportInternalErrorOther(6), + TransportError::IntegerOverflow => CloseError::TransportInternalErrorOther(7), + TransportError::InvalidInput => CloseError::TransportInternalErrorOther(8), + TransportError::InvalidMigration => CloseError::TransportInternalErrorOther(9), + TransportError::InvalidPacket => CloseError::TransportInternalErrorOther(10), + TransportError::InvalidResumptionToken => CloseError::TransportInternalErrorOther(11), + TransportError::InvalidRetry => CloseError::TransportInternalErrorOther(12), + TransportError::InvalidStreamId => CloseError::TransportInternalErrorOther(13), + TransportError::KeysDiscarded(_) => CloseError::TransportInternalErrorOther(14), + TransportError::KeysPending(_) => CloseError::TransportInternalErrorOther(15), + TransportError::KeyUpdateBlocked => CloseError::TransportInternalErrorOther(16), + TransportError::NoMoreData => CloseError::TransportInternalErrorOther(17), + TransportError::NotConnected => CloseError::TransportInternalErrorOther(18), + TransportError::PacketNumberOverlap => CloseError::TransportInternalErrorOther(19), + TransportError::StatelessReset => CloseError::TransportInternalErrorOther(20), + TransportError::TooMuchData => CloseError::TransportInternalErrorOther(21), + TransportError::UnexpectedMessage => CloseError::TransportInternalErrorOther(22), + TransportError::UnknownConnectionId => CloseError::TransportInternalErrorOther(23), + TransportError::UnknownFrameType => CloseError::TransportInternalErrorOther(24), + TransportError::VersionNegotiation => CloseError::TransportInternalErrorOther(25), + TransportError::WrongRole => CloseError::TransportInternalErrorOther(26), + TransportError::QlogError => CloseError::TransportInternalErrorOther(27), + TransportError::NotAvailable => CloseError::TransportInternalErrorOther(28), + TransportError::DisabledVersion => CloseError::TransportInternalErrorOther(29), + } + } +} + +impl From<neqo_transport::ConnectionError> for CloseError { + fn from(error: neqo_transport::ConnectionError) -> CloseError { + match error { + neqo_transport::ConnectionError::Transport(c) => c.into(), + neqo_transport::ConnectionError::Application(c) => CloseError::AppError(c), + } + } +} + +// Reset a stream with streamId. +#[no_mangle] +pub extern "C" fn neqo_http3conn_cancel_fetch( + conn: &mut NeqoHttp3Conn, + stream_id: u64, + error: u64, +) -> nsresult { + match conn.conn.cancel_fetch(StreamId::from(stream_id), error) { + Ok(()) => NS_OK, + Err(_) => NS_ERROR_INVALID_ARG, + } +} + +// Reset a stream with streamId. +#[no_mangle] +pub extern "C" fn neqo_http3conn_reset_stream( + conn: &mut NeqoHttp3Conn, + stream_id: u64, + error: u64, +) -> nsresult { + match conn + .conn + .stream_reset_send(StreamId::from(stream_id), error) + { + Ok(()) => NS_OK, + Err(_) => NS_ERROR_INVALID_ARG, + } +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_stream_stop_sending( + conn: &mut NeqoHttp3Conn, + stream_id: u64, + error: u64, +) -> nsresult { + match conn + .conn + .stream_stop_sending(StreamId::from(stream_id), error) + { + Ok(()) => NS_OK, + Err(_) => NS_ERROR_INVALID_ARG, + } +} + +// Close sending side of a stream with stream_id +#[no_mangle] +pub extern "C" fn neqo_http3conn_close_stream( + conn: &mut NeqoHttp3Conn, + stream_id: u64, +) -> nsresult { + match conn.conn.stream_close_send(StreamId::from(stream_id)) { + Ok(()) => NS_OK, + Err(_) => NS_ERROR_INVALID_ARG, + } +} + +// WebTransport streams can be unidirectional and bidirectional. +// It is mapped to and from neqo's StreamType enum. +#[repr(C)] +pub enum WebTransportStreamType { + BiDi, + UniDi, +} + +impl From<StreamType> for WebTransportStreamType { + fn from(t: StreamType) -> WebTransportStreamType { + match t { + StreamType::BiDi => WebTransportStreamType::BiDi, + StreamType::UniDi => WebTransportStreamType::UniDi, + } + } +} + +impl From<WebTransportStreamType> for StreamType { + fn from(t: WebTransportStreamType) -> StreamType { + match t { + WebTransportStreamType::BiDi => StreamType::BiDi, + WebTransportStreamType::UniDi => StreamType::UniDi, + } + } +} + +#[repr(C)] +pub enum SessionCloseReasonExternal { + Error(u64), + Status(u16), + Clean(u32), +} + +impl SessionCloseReasonExternal { + fn new(reason: SessionCloseReason, data: &mut ThinVec<u8>) -> SessionCloseReasonExternal { + match reason { + SessionCloseReason::Error(e) => SessionCloseReasonExternal::Error(e), + SessionCloseReason::Status(s) => SessionCloseReasonExternal::Status(s), + SessionCloseReason::Clean { error, message } => { + data.extend_from_slice(message.as_ref()); + SessionCloseReasonExternal::Clean(error) + } + } + } +} + +#[repr(C)] +pub enum WebTransportEventExternal { + Negotiated(bool), + Session(u64), + SessionClosed { + stream_id: u64, + reason: SessionCloseReasonExternal, + }, + NewStream { + stream_id: u64, + stream_type: WebTransportStreamType, + session_id: u64, + }, + Datagram { + session_id: u64, + }, +} + +impl WebTransportEventExternal { + fn new(event: WebTransportEvent, data: &mut ThinVec<u8>) -> WebTransportEventExternal { + match event { + WebTransportEvent::Negotiated(n) => WebTransportEventExternal::Negotiated(n), + WebTransportEvent::Session { + stream_id, + status, + headers: _, + } => { + data.extend_from_slice(b"HTTP/3 "); + data.extend_from_slice(&status.to_string().as_bytes()); + data.extend_from_slice(b"\r\n\r\n"); + WebTransportEventExternal::Session(stream_id.as_u64()) + } + WebTransportEvent::SessionClosed { + stream_id, + reason, + headers: _, + } => match reason { + SessionCloseReason::Status(status) => { + data.extend_from_slice(b"HTTP/3 "); + data.extend_from_slice(&status.to_string().as_bytes()); + data.extend_from_slice(b"\r\n\r\n"); + WebTransportEventExternal::Session(stream_id.as_u64()) + } + _ => WebTransportEventExternal::SessionClosed { + stream_id: stream_id.as_u64(), + reason: SessionCloseReasonExternal::new(reason, data), + }, + }, + WebTransportEvent::NewStream { + stream_id, + session_id, + } => WebTransportEventExternal::NewStream { + stream_id: stream_id.as_u64(), + stream_type: stream_id.stream_type().into(), + session_id: session_id.as_u64(), + }, + WebTransportEvent::Datagram { + session_id, + datagram, + } => { + data.extend_from_slice(datagram.as_ref()); + WebTransportEventExternal::Datagram { + session_id: session_id.as_u64(), + } + } + } + } +} + +#[repr(C)] +pub enum Http3Event { + /// A request stream has space for more data to be sent. + DataWritable { + stream_id: u64, + }, + /// A server has send STOP_SENDING frame. + StopSending { + stream_id: u64, + error: u64, + }, + HeaderReady { + stream_id: u64, + fin: bool, + interim: bool, + }, + /// New bytes available for reading. + DataReadable { + stream_id: u64, + }, + /// Peer reset the stream. + Reset { + stream_id: u64, + error: u64, + local: bool, + }, + /// A PushPromise + PushPromise { + push_id: u64, + request_stream_id: u64, + }, + /// A push response headers are ready. + PushHeaderReady { + push_id: u64, + fin: bool, + }, + /// New bytes are available on a push stream for reading. + PushDataReadable { + push_id: u64, + }, + /// A push has been canceled. + PushCanceled { + push_id: u64, + }, + PushReset { + push_id: u64, + error: u64, + }, + RequestsCreatable, + AuthenticationNeeded, + ZeroRttRejected, + ConnectionConnected, + GoawayReceived, + ConnectionClosing { + error: CloseError, + }, + ConnectionClosed { + error: CloseError, + }, + ResumptionToken { + expire_in: u64, // microseconds + }, + EchFallbackAuthenticationNeeded, + WebTransport(WebTransportEventExternal), + NoEvent, +} + +fn sanitize_header(mut y: Cow<[u8]>) -> Cow<[u8]> { + for i in 0..y.len() { + if matches!(y[i], b'\n' | b'\r' | b'\0') { + y.to_mut()[i] = b' '; + } + } + y +} + +fn convert_h3_to_h1_headers(headers: Vec<Header>, ret_headers: &mut ThinVec<u8>) -> nsresult { + if headers.iter().filter(|&h| h.name() == ":status").count() != 1 { + return NS_ERROR_ILLEGAL_VALUE; + } + + let status_val = headers + .iter() + .find(|&h| h.name() == ":status") + .expect("must be one") + .value(); + + ret_headers.extend_from_slice(b"HTTP/3 "); + ret_headers.extend_from_slice(status_val.as_bytes()); + ret_headers.extend_from_slice(b"\r\n"); + + for hdr in headers.iter().filter(|&h| h.name() != ":status") { + ret_headers.extend_from_slice(&sanitize_header(Cow::from(hdr.name().as_bytes()))); + ret_headers.extend_from_slice(b": "); + ret_headers.extend_from_slice(&sanitize_header(Cow::from(hdr.value().as_bytes()))); + ret_headers.extend_from_slice(b"\r\n"); + } + ret_headers.extend_from_slice(b"\r\n"); + return NS_OK; +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_event( + conn: &mut NeqoHttp3Conn, + ret_event: &mut Http3Event, + data: &mut ThinVec<u8>, +) -> nsresult { + while let Some(evt) = conn.conn.next_event() { + let fe = match evt { + Http3ClientEvent::DataWritable { stream_id } => Http3Event::DataWritable { + stream_id: stream_id.as_u64(), + }, + Http3ClientEvent::StopSending { stream_id, error } => Http3Event::StopSending { + stream_id: stream_id.as_u64(), + error, + }, + Http3ClientEvent::HeaderReady { + stream_id, + headers, + fin, + interim, + } => { + let res = convert_h3_to_h1_headers(headers, data); + if res != NS_OK { + return res; + } + Http3Event::HeaderReady { + stream_id: stream_id.as_u64(), + fin, + interim, + } + } + Http3ClientEvent::DataReadable { stream_id } => Http3Event::DataReadable { + stream_id: stream_id.as_u64(), + }, + Http3ClientEvent::Reset { + stream_id, + error, + local, + } => Http3Event::Reset { + stream_id: stream_id.as_u64(), + error, + local, + }, + Http3ClientEvent::PushPromise { + push_id, + request_stream_id, + headers, + } => { + let res = convert_h3_to_h1_headers(headers, data); + if res != NS_OK { + return res; + } + Http3Event::PushPromise { + push_id, + request_stream_id: request_stream_id.as_u64(), + } + } + Http3ClientEvent::PushHeaderReady { + push_id, + headers, + fin, + interim, + } => { + if interim { + Http3Event::NoEvent + } else { + let res = convert_h3_to_h1_headers(headers, data); + if res != NS_OK { + return res; + } + Http3Event::PushHeaderReady { push_id, fin } + } + } + Http3ClientEvent::PushDataReadable { push_id } => { + Http3Event::PushDataReadable { push_id } + } + Http3ClientEvent::PushCanceled { push_id } => Http3Event::PushCanceled { push_id }, + Http3ClientEvent::PushReset { push_id, error } => { + Http3Event::PushReset { push_id, error } + } + Http3ClientEvent::RequestsCreatable => Http3Event::RequestsCreatable, + Http3ClientEvent::AuthenticationNeeded => Http3Event::AuthenticationNeeded, + Http3ClientEvent::ZeroRttRejected => Http3Event::ZeroRttRejected, + Http3ClientEvent::ResumptionToken(token) => { + // expiration_time time is Instant, transform it into microseconds it will + // be valid for. Necko code will add the value to PR_Now() to get the expiration + // time in PRTime. + if token.expiration_time() > Instant::now() { + let e = (token.expiration_time() - Instant::now()).as_micros(); + if let Ok(expire_in) = u64::try_from(e) { + data.extend_from_slice(token.as_ref()); + Http3Event::ResumptionToken { expire_in } + } else { + Http3Event::NoEvent + } + } else { + Http3Event::NoEvent + } + } + Http3ClientEvent::GoawayReceived => Http3Event::GoawayReceived, + Http3ClientEvent::StateChange(state) => match state { + Http3State::Connected => Http3Event::ConnectionConnected, + Http3State::Closing(error_code) => { + match error_code { + neqo_transport::ConnectionError::Transport( + TransportError::CryptoError(neqo_crypto::Error::EchRetry(ref c)), + ) + | neqo_transport::ConnectionError::Transport(TransportError::EchRetry( + ref c, + )) => { + data.extend_from_slice(c.as_ref()); + } + _ => {} + } + Http3Event::ConnectionClosing { + error: error_code.into(), + } + } + Http3State::Closed(error_code) => { + match error_code { + neqo_transport::ConnectionError::Transport( + TransportError::CryptoError(neqo_crypto::Error::EchRetry(ref c)), + ) + | neqo_transport::ConnectionError::Transport(TransportError::EchRetry( + ref c, + )) => { + data.extend_from_slice(c.as_ref()); + } + _ => {} + } + Http3Event::ConnectionClosed { + error: error_code.into(), + } + } + _ => Http3Event::NoEvent, + }, + Http3ClientEvent::EchFallbackAuthenticationNeeded { public_name } => { + data.extend_from_slice(public_name.as_ref()); + Http3Event::EchFallbackAuthenticationNeeded + } + Http3ClientEvent::WebTransport(e) => { + Http3Event::WebTransport(WebTransportEventExternal::new(e, data)) + } + }; + + if !matches!(fe, Http3Event::NoEvent) { + *ret_event = fe; + return NS_OK; + } + } + + *ret_event = Http3Event::NoEvent; + NS_OK +} + +// Read response data into buf. +#[no_mangle] +pub unsafe extern "C" fn neqo_http3conn_read_response_data( + conn: &mut NeqoHttp3Conn, + stream_id: u64, + buf: *mut u8, + len: u32, + read: &mut u32, + fin: &mut bool, +) -> nsresult { + let array = slice::from_raw_parts_mut(buf, len as usize); + match conn + .conn + .read_data(Instant::now(), StreamId::from(stream_id), &mut array[..]) + { + Ok((amount, fin_recvd)) => { + *read = u32::try_from(amount).unwrap(); + *fin = fin_recvd; + if (amount == 0) && !fin_recvd { + NS_BASE_STREAM_WOULD_BLOCK + } else { + NS_OK + } + } + Err(Http3Error::InvalidStreamId) + | Err(Http3Error::TransportError(TransportError::NoMoreData)) => NS_ERROR_INVALID_ARG, + Err(_) => NS_ERROR_NET_HTTP3_PROTOCOL_ERROR, + } +} + +#[repr(C)] +pub struct NeqoSecretInfo { + set: bool, + version: u16, + cipher: u16, + group: u16, + resumed: bool, + early_data: bool, + alpn: nsCString, + signature_scheme: u16, + ech_accepted: bool, +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_tls_info( + conn: &mut NeqoHttp3Conn, + sec_info: &mut NeqoSecretInfo, +) -> nsresult { + match conn.conn.tls_info() { + Some(info) => { + sec_info.set = true; + sec_info.version = info.version(); + sec_info.cipher = info.cipher_suite(); + sec_info.group = info.key_exchange(); + sec_info.resumed = info.resumed(); + sec_info.early_data = info.early_data_accepted(); + sec_info.alpn = match info.alpn() { + Some(a) => nsCString::from(a), + None => nsCString::new(), + }; + sec_info.signature_scheme = info.signature_scheme(); + sec_info.ech_accepted = info.ech_accepted(); + NS_OK + } + None => NS_ERROR_NOT_AVAILABLE, + } +} + +#[repr(C)] +pub struct NeqoCertificateInfo { + certs: ThinVec<ThinVec<u8>>, + stapled_ocsp_responses_present: bool, + stapled_ocsp_responses: ThinVec<ThinVec<u8>>, + signed_cert_timestamp_present: bool, + signed_cert_timestamp: ThinVec<u8>, +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_peer_certificate_info( + conn: &mut NeqoHttp3Conn, + neqo_certs_info: &mut NeqoCertificateInfo, +) -> nsresult { + let mut certs_info = match conn.conn.peer_certificate() { + Some(certs) => certs, + None => return NS_ERROR_NOT_AVAILABLE, + }; + + neqo_certs_info.certs = certs_info + .map(|cert| cert.iter().cloned().collect()) + .collect(); + + match &mut certs_info.stapled_ocsp_responses() { + Some(ocsp_val) => { + neqo_certs_info.stapled_ocsp_responses_present = true; + neqo_certs_info.stapled_ocsp_responses = ocsp_val + .iter() + .map(|ocsp| ocsp.iter().cloned().collect()) + .collect(); + } + None => { + neqo_certs_info.stapled_ocsp_responses_present = false; + } + }; + + match certs_info.signed_cert_timestamp() { + Some(sct_val) => { + neqo_certs_info.signed_cert_timestamp_present = true; + neqo_certs_info + .signed_cert_timestamp + .extend_from_slice(sct_val); + } + None => { + neqo_certs_info.signed_cert_timestamp_present = false; + } + }; + + NS_OK +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_authenticated(conn: &mut NeqoHttp3Conn, error: PRErrorCode) { + conn.conn.authenticated(error.into(), Instant::now()); +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_set_resumption_token( + conn: &mut NeqoHttp3Conn, + token: &mut ThinVec<u8>, +) { + let _ = conn.conn.enable_resumption(Instant::now(), token); +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_set_ech_config( + conn: &mut NeqoHttp3Conn, + ech_config: &mut ThinVec<u8>, +) { + let _ = conn.conn.enable_ech(ech_config); +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_is_zero_rtt(conn: &mut NeqoHttp3Conn) -> bool { + conn.conn.state() == Http3State::ZeroRtt +} + +#[repr(C)] +#[derive(Default)] +pub struct Http3Stats { + /// Total packets received, including all the bad ones. + pub packets_rx: usize, + /// Duplicate packets received. + pub dups_rx: usize, + /// Dropped packets or dropped garbage. + pub dropped_rx: usize, + /// The number of packet that were saved for later processing. + pub saved_datagrams: usize, + /// Total packets sent. + pub packets_tx: usize, + /// Total number of packets that are declared lost. + pub lost: usize, + /// Late acknowledgments, for packets that were declared lost already. + pub late_ack: usize, + /// Acknowledgments for packets that contained data that was marked + /// for retransmission when the PTO timer popped. + pub pto_ack: usize, + /// Count PTOs. Single PTOs, 2 PTOs in a row, 3 PTOs in row, etc. are counted + /// separately. + pub pto_counts: [usize; 16], +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_get_stats(conn: &mut NeqoHttp3Conn, stats: &mut Http3Stats) { + let t_stats = conn.conn.transport_stats(); + stats.packets_rx = t_stats.packets_rx; + stats.dups_rx = t_stats.dups_rx; + stats.dropped_rx = t_stats.dropped_rx; + stats.saved_datagrams = t_stats.saved_datagrams; + stats.packets_tx = t_stats.packets_tx; + stats.lost = t_stats.lost; + stats.late_ack = t_stats.late_ack; + stats.pto_ack = t_stats.pto_ack; + stats.pto_counts = t_stats.pto_counts; +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_webtransport_create_session( + conn: &mut NeqoHttp3Conn, + host: &nsACString, + path: &nsACString, + headers: &nsACString, + stream_id: &mut u64, +) -> nsresult { + let hdrs = match parse_headers(headers) { + Err(e) => { + return e; + } + Ok(h) => h, + }; + let host_tmp = match str::from_utf8(host) { + Ok(h) => h, + Err(_) => { + return NS_ERROR_INVALID_ARG; + } + }; + let path_tmp = match str::from_utf8(path) { + Ok(p) => p, + Err(_) => { + return NS_ERROR_INVALID_ARG; + } + }; + + match conn.conn.webtransport_create_session( + Instant::now(), + &("https", host_tmp, path_tmp), + &hdrs, + ) { + Ok(id) => { + *stream_id = id.as_u64(); + NS_OK + } + Err(Http3Error::StreamLimitError) => NS_BASE_STREAM_WOULD_BLOCK, + Err(_) => NS_ERROR_UNEXPECTED, + } +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_webtransport_close_session( + conn: &mut NeqoHttp3Conn, + session_id: u64, + error: u32, + message: &nsACString, +) -> nsresult { + let message_tmp = match str::from_utf8(message) { + Ok(p) => p, + Err(_) => { + return NS_ERROR_INVALID_ARG; + } + }; + match conn + .conn + .webtransport_close_session(StreamId::from(session_id), error, message_tmp) + { + Ok(()) => NS_OK, + Err(_) => NS_ERROR_INVALID_ARG, + } +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_webtransport_create_stream( + conn: &mut NeqoHttp3Conn, + session_id: u64, + stream_type: WebTransportStreamType, + stream_id: &mut u64, +) -> nsresult { + match conn + .conn + .webtransport_create_stream(StreamId::from(session_id), stream_type.into()) + { + Ok(id) => { + *stream_id = id.as_u64(); + NS_OK + } + Err(Http3Error::StreamLimitError) => NS_BASE_STREAM_WOULD_BLOCK, + Err(_) => NS_ERROR_UNEXPECTED, + } +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_webtransport_send_datagram( + conn: &mut NeqoHttp3Conn, + session_id: u64, + data: &mut ThinVec<u8>, + tracking_id: u64, +) -> nsresult { + let id = if tracking_id == 0 { + None + } else { + Some(tracking_id) + }; + match conn + .conn + .webtransport_send_datagram(StreamId::from(session_id), data, id) + { + Ok(()) => NS_OK, + Err(Http3Error::TransportError(TransportError::TooMuchData)) => NS_ERROR_NOT_AVAILABLE, + Err(_) => NS_ERROR_UNEXPECTED, + } +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_webtransport_max_datagram_size( + conn: &mut NeqoHttp3Conn, + session_id: u64, + result: &mut u64, +) -> nsresult { + match conn + .conn + .webtransport_max_datagram_size(StreamId::from(session_id)) + { + Ok(size) => { + *result = size; + NS_OK + } + Err(_) => NS_ERROR_UNEXPECTED, + } +} |