diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /netwerk/socket | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'netwerk/socket')
25 files changed, 4692 insertions, 0 deletions
diff --git a/netwerk/socket/moz.build b/netwerk/socket/moz.build new file mode 100644 index 0000000000..e42e16a1e2 --- /dev/null +++ b/netwerk/socket/moz.build @@ -0,0 +1,49 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +DIRS += [ + "neqo_glue", +] + +XPIDL_SOURCES += [ + "nsISocketProvider.idl", + "nsISocketProviderService.idl", + "nsISOCKSSocketInfo.idl", + "nsISSLSocketControl.idl", + "nsITransportSecurityInfo.idl", +] + +XPIDL_MODULE = "necko_socket" + +LOCAL_INCLUDES += [ + "/netwerk/base", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +EXPORTS += [ + "nsSocketProviderService.h", +] + +UNIFIED_SOURCES += [ + "nsSocketProviderService.cpp", + "nsSOCKSIOLayer.cpp", + "nsSOCKSSocketProvider.cpp", + "nsUDPSocketProvider.cpp", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + XPIDL_SOURCES += [ + "nsINamedPipeService.idl", + ] + EXPORTS += [ + "nsNamedPipeService.h", + ] + UNIFIED_SOURCES += ["nsNamedPipeIOLayer.cpp", "nsNamedPipeService.cpp"] + +FINAL_LIBRARY = "xul" + +CONFIGURE_SUBST_FILES += ["neqo/extra-bindgen-flags"] diff --git a/netwerk/socket/neqo/extra-bindgen-flags.in b/netwerk/socket/neqo/extra-bindgen-flags.in new file mode 100644 index 0000000000..ddf9e8df49 --- /dev/null +++ b/netwerk/socket/neqo/extra-bindgen-flags.in @@ -0,0 +1 @@ +@BINDGEN_SYSTEM_FLAGS@ diff --git a/netwerk/socket/neqo_glue/Cargo.toml b/netwerk/socket/neqo_glue/Cargo.toml new file mode 100644 index 0000000000..84ef23841c --- /dev/null +++ b/netwerk/socket/neqo_glue/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "neqo_glue" +version = "0.1.0" +authors = ["Dragana Damjanovic <dd.mozilla@gmail.com>"] +edition = "2018" + +[lib] +name = "neqo_glue" + +[dependencies] +neqo-http3 = { tag = "v0.4.19", git = "https://github.com/mozilla/neqo" } +neqo-transport = { tag = "v0.4.19", git = "https://github.com/mozilla/neqo" } +neqo-common = { tag = "v0.4.19", git = "https://github.com/mozilla/neqo" } +neqo-qpack = { tag = "v0.4.19", git = "https://github.com/mozilla/neqo" } +nserror = { path = "../../../xpcom/rust/nserror" } +nsstring = { path = "../../../xpcom/rust/nsstring" } +xpcom = { path = "../../../xpcom/rust/xpcom" } +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +log = "0.4.0" +qlog = "0.4.0" + +[dependencies.neqo-crypto] +tag = "v0.4.19" +git = "https://github.com/mozilla/neqo" +default-features = false +features = ["gecko"] diff --git a/netwerk/socket/neqo_glue/NeqoHttp3Conn.h b/netwerk/socket/neqo_glue/NeqoHttp3Conn.h new file mode 100644 index 0000000000..c2ba79fac2 --- /dev/null +++ b/netwerk/socket/neqo_glue/NeqoHttp3Conn.h @@ -0,0 +1,107 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef NeqoHttp3Conn_h__ +#define NeqoHttp3Conn_h__ + +#include "mozilla/net/neqo_glue_ffi_generated.h" + +namespace mozilla { +namespace net { + +class NeqoHttp3Conn final { + public: + static nsresult Init(const nsACString& aOrigin, const nsACString& aAlpn, + const nsACString& aLocalAddr, + const nsACString& aRemoteAddr, uint32_t aMaxTableSize, + uint16_t aMaxBlockedStreams, const nsACString& aQlogDir, + NeqoHttp3Conn** aConn) { + return neqo_http3conn_new(&aOrigin, &aAlpn, &aLocalAddr, &aRemoteAddr, + aMaxTableSize, aMaxBlockedStreams, &aQlogDir, + (const mozilla::net::NeqoHttp3Conn**)aConn); + } + + void Close(uint64_t aError) { neqo_http3conn_close(this, aError); } + + nsresult GetSecInfo(NeqoSecretInfo* aSecInfo) { + return neqo_http3conn_tls_info(this, aSecInfo); + } + + nsresult PeerCertificateInfo(NeqoCertificateInfo* aCertInfo) { + return neqo_http3conn_peer_certificate_info(this, aCertInfo); + } + + void PeerAuthenticated(PRErrorCode aError) { + neqo_http3conn_authenticated(this, aError); + } + + void ProcessInput(uint8_t* aPacket, uint32_t aLen) { + neqo_http3conn_process_input(this, aPacket, aLen); + } + + uint64_t ProcessOutput() { return neqo_http3conn_process_output(this); } + + bool HasDataToSend() { return neqo_http3conn_has_data_to_send(this); } + + nsresult GetDataToSend(nsTArray<uint8_t>& aData) { + aData.TruncateLength(0); + return neqo_http3conn_get_data_to_send(this, &aData); + } + + nsresult GetEvent(Http3Event* aEvent, nsTArray<uint8_t>& aData) { + return neqo_http3conn_event(this, aEvent, &aData); + } + + nsresult Fetch(const nsACString& aMethod, const nsACString& aScheme, + const nsACString& aHost, const nsACString& aPath, + const nsACString& aHeaders, uint64_t* aStreamId) { + return neqo_http3conn_fetch(this, &aMethod, &aScheme, &aHost, &aPath, + &aHeaders, aStreamId); + } + + nsresult SendRequestBody(uint64_t aStreamId, const uint8_t* aBuf, + uint32_t aCount, uint32_t* aCountRead) { + return neqo_htttp3conn_send_request_body(this, aStreamId, aBuf, aCount, + aCountRead); + } + + // This closes only the sending side of a stream. + nsresult CloseStream(uint64_t aStreamId) { + return neqo_http3conn_close_stream(this, aStreamId); + } + + nsresult ReadResponseData(uint64_t aStreamId, uint8_t* aBuf, uint32_t aLen, + uint32_t* aRead, bool* aFin) { + return neqo_http3conn_read_response_data(this, aStreamId, aBuf, aLen, aRead, + aFin); + } + + void ResetStream(uint64_t aStreamId, uint64_t aError) { + neqo_http3conn_reset_stream(this, aStreamId, aError); + } + + void SetResumptionToken(nsTArray<uint8_t>& aToken) { + neqo_http3conn_set_resumption_token(this, &aToken); + } + + bool IsZeroRtt() { return neqo_http3conn_is_zero_rtt(this); } + + nsrefcnt AddRef() { return neqo_http3conn_addref(this); } + nsrefcnt Release() { return neqo_http3conn_release(this); } + + void GetStats(Http3Stats* aStats) { + return neqo_http3conn_get_stats(this, aStats); + } + + private: + NeqoHttp3Conn() = delete; + ~NeqoHttp3Conn() = delete; + NeqoHttp3Conn(const NeqoHttp3Conn&) = delete; + NeqoHttp3Conn& operator=(const NeqoHttp3Conn&) = delete; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/socket/neqo_glue/cbindgen.toml b/netwerk/socket/neqo_glue/cbindgen.toml new file mode 100644 index 0000000000..406c8dbc66 --- /dev/null +++ b/netwerk/socket/neqo_glue/cbindgen.toml @@ -0,0 +1,26 @@ +header = """/* 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 http://mozilla.org/MPL/2.0/. */""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. + */ + +namespace mozilla { +namespace net { +class NeqoHttp3Conn; +} // namespace net +} // namespace mozilla + """ +include_version = true +braces = "SameLine" +line_length = 100 +tab_width = 2 +language = "C++" +namespaces = ["mozilla", "net"] +includes = ["certt.h", "prerror.h"] + +[export] +exclude = ["NeqoHttp3Conn"] +item_types = ["globals", "enums", "structs", "unions", "typedefs", "opaque", "functions", "constants"] + +[export.rename] +"ThinVec" = "nsTArray" diff --git a/netwerk/socket/neqo_glue/moz.build b/netwerk/socket/neqo_glue/moz.build new file mode 100644 index 0000000000..b3123dde4f --- /dev/null +++ b/netwerk/socket/neqo_glue/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.net += [ + "NeqoHttp3Conn.h", +] + +LOCAL_INCLUDES += [ + "/security/manager/ssl", + "/security/nss/lib/ssl", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + CbindgenHeader("neqo_glue_ffi_generated.h", inputs=["/netwerk/socket/neqo_glue"]) + + EXPORTS.mozilla.net += [ + "!neqo_glue_ffi_generated.h", + ] diff --git a/netwerk/socket/neqo_glue/src/lib.rs b/netwerk/socket/neqo_glue/src/lib.rs new file mode 100644 index 0000000000..9d04aba3f5 --- /dev/null +++ b/netwerk/socket/neqo_glue/src/lib.rs @@ -0,0 +1,816 @@ +/* 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/. */ + +use neqo_common::event::Provider; +use neqo_common::{self as common, qlog::NeqoQlog, qwarn, Datagram, Role}; +use neqo_crypto::{init, PRErrorCode}; +use neqo_http3::Error as Http3Error; +use neqo_http3::{Http3Client, Http3ClientEvent, Http3Parameters, Http3State}; +use neqo_qpack::QpackSettings; +use neqo_transport::{ + ConnectionParameters, Error as TransportError, FixedConnectionIdManager, Output, QuicVersion, +}; +use nserror::*; +use nsstring::*; +use qlog::QlogStreamer; +use std::cell::RefCell; +use std::convert::TryFrom; +use std::fs::OpenOptions; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::ptr; +use std::rc::Rc; +use std::slice; +use std::str; +use std::time::Instant; +use thin_vec::ThinVec; +use xpcom::{interfaces::nsrefcnt, AtomicRefcnt, RefCounted, RefPtr}; + +#[repr(C)] +pub struct NeqoHttp3Conn { + conn: Http3Client, + local_addr: SocketAddr, + remote_addr: SocketAddr, + refcnt: AtomicRefcnt, + packets_to_send: Vec<Datagram>, +} + +impl NeqoHttp3Conn { + fn new( + origin: &nsACString, + alpn: &nsACString, + local_addr: &nsACString, + remote_addr: &nsACString, + max_table_size: u64, + max_blocked_streams: u16, + qlog_dir: &nsACString, + ) -> 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 = match str::from_utf8(local_addr) { + Ok(s) => match s.parse() { + Ok(addr) => addr, + Err(_) => return Err(NS_ERROR_INVALID_ARG), + }, + Err(_) => return Err(NS_ERROR_INVALID_ARG), + }; + + let remote: SocketAddr = match str::from_utf8(remote_addr) { + Ok(s) => match s.parse() { + Ok(addr) => addr, + Err(_) => return Err(NS_ERROR_INVALID_ARG), + }, + Err(_) => return Err(NS_ERROR_INVALID_ARG), + }; + + let http3_settings = Http3Parameters { + qpack_settings: QpackSettings { + max_table_size_encoder: max_table_size, + max_table_size_decoder: max_table_size, + max_blocked_streams, + }, + max_concurrent_push_streams: 0, + }; + + let quic_version = match alpn_conv { + "h3-30" => QuicVersion::Draft30, + "h3-29" => QuicVersion::Draft29, + "h3-28" => QuicVersion::Draft28, + "h3-27" => QuicVersion::Draft27, + _ => return Err(NS_ERROR_INVALID_ARG), + }; + + let mut conn = match Http3Client::new( + origin_conv, + Rc::new(RefCell::new(FixedConnectionIdManager::new(3))), + local, + remote, + &ConnectionParameters::default().quic_version(quic_version), + &http3_settings, + ) { + 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, + remote_addr: remote, + refcnt: unsafe { AtomicRefcnt::new() }, + packets_to_send: Vec::new(), + })); + unsafe { Ok(RefPtr::from_raw(conn).unwrap()) } + } +} + +#[no_mangle] +pub unsafe extern "C" fn neqo_http3conn_addref(conn: &NeqoHttp3Conn) -> nsrefcnt { + conn.refcnt.inc() +} + +#[no_mangle] +pub unsafe extern "C" fn neqo_http3conn_release(conn: &NeqoHttp3Conn) -> nsrefcnt { + let rc = conn.refcnt.dec(); + if rc == 0 { + Box::from_raw(conn as *const _ as *mut NeqoHttp3Conn); + } + rc +} + +// 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: &nsACString, + remote_addr: &nsACString, + max_table_size: u64, + max_blocked_streams: u16, + qlog_dir: &nsACString, + result: &mut *const NeqoHttp3Conn, +) -> nsresult { + *result = ptr::null_mut(); + + match NeqoHttp3Conn::new( + origin, + alpn, + local_addr, + remote_addr, + max_table_size, + max_blocked_streams, + qlog_dir, + ) { + Ok(http3_conn) => { + http3_conn.forget(result); + NS_OK + } + Err(e) => e, + } +} + +/* Process a packet. + * packet holds packet data. + */ +#[no_mangle] +pub extern "C" fn neqo_http3conn_process_input( + conn: &mut NeqoHttp3Conn, + packet: *const u8, + len: u32, +) { + let array = unsafe { slice::from_raw_parts(packet, len as usize) }; + conn.conn.process_input( + Datagram::new(conn.remote_addr, conn.local_addr, array.to_vec()), + Instant::now(), + ); +} + +/* Process output and store data to be sent into conn.packets_to_send. + * neqo_http3conn_get_data_to_send will be called to pick up this data. + */ +#[no_mangle] +pub extern "C" fn neqo_http3conn_process_output(conn: &mut NeqoHttp3Conn) -> u64 { + loop { + let out = conn.conn.process_output(Instant::now()); + match out { + Output::Datagram(dg) => { + conn.packets_to_send.push(dg); + } + Output::Callback(to) => { + let 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 { + break 1; + } + break timeout; + } + Output::None => break std::u64::MAX, + } + } +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_has_data_to_send(conn: &mut NeqoHttp3Conn) -> bool { + !conn.packets_to_send.is_empty() +} + +#[no_mangle] +pub extern "C" fn neqo_http3conn_get_data_to_send( + conn: &mut NeqoHttp3Conn, + packet: &mut ThinVec<u8>, +) -> nsresult { + match conn.packets_to_send.pop() { + None => NS_BASE_STREAM_WOULD_BLOCK, + Some(d) => { + packet.extend_from_slice(&d); + NS_OK + } + } +} + +#[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 + } +} + +#[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, +) -> 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 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((name, value)); + } + } + } + + 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; + } + }; + match conn.conn.fetch( + Instant::now(), + method_tmp, + scheme_tmp, + host_tmp, + path_tmp, + &hdrs, + ) { + Ok(id) => { + *stream_id = id; + NS_OK + } + Err(Http3Error::StreamLimitError) => NS_BASE_STREAM_WOULD_BLOCK, + Err(_) => NS_ERROR_UNEXPECTED, + } +} + +#[no_mangle] +pub 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 = unsafe { slice::from_raw_parts(buf, len as usize) }; + match conn.conn.send_request_body(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, + } +} + +// 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 { + QuicTransportError(u64), + Http3AppError(u64), +} + +impl From<neqo_transport::CloseError> for CloseError { + fn from(error: neqo_transport::CloseError) -> CloseError { + match error { + neqo_transport::CloseError::Transport(c) => CloseError::QuicTransportError(c), + neqo_transport::CloseError::Application(c) => CloseError::Http3AppError(c), + } + } +} + +// 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(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(stream_id) { + Ok(()) => NS_OK, + Err(_) => NS_ERROR_INVALID_ARG, + } +} + +#[repr(C)] +pub enum Http3Event { + /// A request stream has space for more data to be send. + DataWritable { + stream_id: u64, + }, + /// A server has send STOP_SENDING frame. + StopSending { + stream_id: u64, + error: u64, + }, + HeaderReady { + stream_id: u64, + fin: 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 + }, + NoEvent, +} + +fn convert_h3_to_h1_headers( + headers: Vec<(String, String)>, + ret_headers: &mut ThinVec<u8>, +) -> nsresult { + if headers.iter().filter(|(k, _)| k == ":status").count() != 1 { + return NS_ERROR_ILLEGAL_VALUE; + } + + let (_, status_val) = headers + .iter() + .find(|(k, _)| k == ":status") + .expect("must be one"); + + 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 (key, value) in headers.iter().filter(|(k, _)| k != ":status") { + ret_headers.extend_from_slice(key.as_bytes()); + ret_headers.extend_from_slice(b": "); + ret_headers.extend_from_slice(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 }, + Http3ClientEvent::StopSending { stream_id, error } => { + Http3Event::StopSending { stream_id, error } + } + Http3ClientEvent::HeaderReady { + stream_id, + headers, + fin, + interim, + } => { + if interim { + // This are 1xx responses and they are ignored. + Http3Event::NoEvent + } else { + let res = convert_h3_to_h1_headers(headers, data); + if res != NS_OK { + return res; + } + Http3Event::HeaderReady { stream_id, fin } + } + } + Http3ClientEvent::DataReadable { stream_id } => Http3Event::DataReadable { stream_id }, + Http3ClientEvent::Reset { + stream_id, + error, + local, + } => Http3Event::Reset { + stream_id, + 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, + } + } + 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) => Http3Event::ConnectionClosing { + error: error_code.into(), + }, + Http3State::Closed(error_code) => Http3Event::ConnectionClosed { + error: error_code.into(), + }, + _ => Http3Event::NoEvent, + }, + }; + + 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 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 = unsafe { slice::from_raw_parts_mut(buf, len as usize) }; + match conn + .conn + .read_response_data(Instant::now(), 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, +} + +#[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(); + 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_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; +} diff --git a/netwerk/socket/nsINamedPipeService.idl b/netwerk/socket/nsINamedPipeService.idl new file mode 100644 index 0000000000..9db557577d --- /dev/null +++ b/netwerk/socket/nsINamedPipeService.idl @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsrootidl.idl" + +/** + * nsINamedPipeDataObserver + * + * This is the callback interface for nsINamedPipeService. + * The functions are called by the internal thread in the nsINamedPipeService. + */ +[scriptable, uuid(de4f460b-94fd-442c-9002-1637beb2185a)] +interface nsINamedPipeDataObserver : nsISupports +{ + /** + * onDataAvailable + * + * @param aBytesTransferred + * Transfered bytes during last transmission. + * @param aOverlapped + * Corresponding overlapped structure used by the async I/O + */ + void onDataAvailable(in unsigned long aBytesTransferred, + in voidPtr aOverlapped); + + /** + * onError + * + * @param aError + * Error code of the error. + * @param aOverlapped + * Corresponding overlapped structure used by the async I/O + */ + void onError(in unsigned long aError, + in voidPtr aOverlapped); +}; + +/** + * nsINamedPipeService + */ +[scriptable, uuid(1bf19133-5625-4ac8-836a-80b1c215f72b)] +interface nsINamedPipeService : nsISupports +{ + /** + * addDataObserver + * + * @param aHandle + * The handle that is going to be monitored for read/write operations. + * Only handles that are opened with overlapped IO are supported. + * @param aObserver + * The observer of the handle, the service strong-refs of the observer. + */ + void addDataObserver(in voidPtr aHandle, + in nsINamedPipeDataObserver aObserver); + + /** + * removeDataObserver + * + * @param aHandle + The handle associated to the observer, and will be closed by the + service. + * @param aObserver + * The observer to be removed. + */ + void removeDataObserver(in voidPtr aHandle, + in nsINamedPipeDataObserver aObserver); + + /** + * isOnCurrentThread + * + * @return the caller runs within the internal thread. + */ + boolean isOnCurrentThread(); +}; diff --git a/netwerk/socket/nsISOCKSSocketInfo.idl b/netwerk/socket/nsISOCKSSocketInfo.idl new file mode 100644 index 0000000000..b17176f53b --- /dev/null +++ b/netwerk/socket/nsISOCKSSocketInfo.idl @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +%{ C++ +namespace mozilla { +namespace net { +union NetAddr; +} +} +%} +[ptr] native NetAddrPtr(mozilla::net::NetAddr); + +[scriptable, uuid(D5C0D1F9-22D7-47DC-BF91-D9AC6E1251A6)] +interface nsISOCKSSocketInfo : nsISupports +{ + [noscript] attribute NetAddrPtr destinationAddr; + [noscript] attribute NetAddrPtr externalProxyAddr; + [noscript] attribute NetAddrPtr internalProxyAddr; +}; diff --git a/netwerk/socket/nsISSLSocketControl.idl b/netwerk/socket/nsISSLSocketControl.idl new file mode 100644 index 0000000000..41e653177f --- /dev/null +++ b/netwerk/socket/nsISSLSocketControl.idl @@ -0,0 +1,170 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIInterfaceRequestor; +interface nsIX509Cert; + +%{C++ +#include "nsStringFwd.h" +#include "nsTArrayForwardDeclare.h" +%} +[ref] native nsCStringTArrayRef(nsTArray<nsCString>); + +[scriptable, builtinclass, uuid(418265c8-654e-4fbb-ba62-4eed27de1f03)] +interface nsISSLSocketControl : nsISupports { + attribute nsIInterfaceRequestor notificationCallbacks; + + void proxyStartSSL(); + void StartTLS(); + + /* NPN (Next Protocol Negotiation) is a mechanism for + negotiating the protocol to be spoken inside the SSL + tunnel during the SSL handshake. The NPNList is the list + of offered client side protocols. setNPNList() needs to + be called before any data is read or written (including the + handshake to be setup correctly. The server determines the + priority when multiple matches occur, but if there is no overlap + the first protocol in the list is used. */ + + [noscript] void setNPNList(in nsCStringTArrayRef aNPNList); + + /* For 0RTT we need to know the alpn protocol selected for the last tls + * session. This function will return a value if applicable or an error + * NS_ERROR_NOT_AVAILABLE. + */ + ACString getAlpnEarlySelection(); + + /* If 0RTT handshake was applied and some data has been sent, as soon as + * the handshake finishes this attribute will be set to appropriate value. + */ + readonly attribute bool earlyDataAccepted; + + /* When 0RTT is performed, PR_Write will not drive the handshake forward. + * It must be forced by calling this function. + */ + void driveHandshake(); + + /* Determine if a potential SSL connection to hostname:port with + * a desired NPN negotiated protocol of npnProtocol can use the socket + * associated with this object instead of making a new one. And if so, combine + * them. + */ + boolean joinConnection( + in ACString npnProtocol, /* e.g. "h2" */ + in ACString hostname, + in long port); + + /* just like JoinConnection() except do not mark a successful test as joined. + */ + boolean testJoinConnection( + in ACString npnProtocol, /* e.g. "h2" */ + in ACString hostname, + in long port); + + /* Determine if existing connection should be trusted to convey information about + * a hostname. + */ + boolean isAcceptableForHost(in ACString hostname); + + /* The Key Exchange Algorithm is used when determining whether or + not HTTP/2 can be used. + + After a handshake is complete it can be read from KEAUsed. + The values correspond to the SSLKEAType enum in NSS or the + KEY_EXCHANGE_UNKNOWN constant defined below. + + KEAKeyBits is the size/security-level used for the KEA. + */ + + [infallible] readonly attribute short KEAUsed; + [infallible] readonly attribute unsigned long KEAKeyBits; + + const short KEY_EXCHANGE_UNKNOWN = -1; + + /* + * The original flags from the socket provider. + */ + readonly attribute uint32_t providerFlags; + + /* + * The original TLS flags from the socket provider. + */ + readonly attribute uint32_t providerTlsFlags; + + /* These values are defined by TLS. */ + const short SSL_VERSION_3 = 0x0300; + const short TLS_VERSION_1 = 0x0301; + const short TLS_VERSION_1_1 = 0x0302; + const short TLS_VERSION_1_2 = 0x0303; + const short TLS_VERSION_1_3 = 0x0304; + const short SSL_VERSION_UNKNOWN = -1; + + [infallible] readonly attribute short SSLVersionUsed; + [infallible] readonly attribute short SSLVersionOffered; + + /* These values match the NSS defined values in sslt.h */ + const short SSL_MAC_UNKNOWN = -1; + const short SSL_MAC_NULL = 0; + const short SSL_MAC_MD5 = 1; + const short SSL_MAC_SHA = 2; + const short SSL_HMAC_MD5 = 3; + const short SSL_HMAC_SHA = 4; + const short SSL_HMAC_SHA256 = 5; + const short SSL_MAC_AEAD = 6; + + [infallible] readonly attribute short MACAlgorithmUsed; + + /** + * If set to true before the server requests a client cert + * no cert will be sent. + */ + [notxpcom, nostdcall] attribute boolean denyClientCert; + + /** + * If set before the server requests a client cert (assuming it does so at + * all), then this cert will be presented to the server, instead of asking + * the user or searching the set of rememebered user cert decisions. + */ + attribute nsIX509Cert clientCert; + + /** + * True iff a client cert has been sent to the server - i.e. this + * socket has been client-cert authenticated. + */ + [infallible] readonly attribute boolean clientCertSent; + + /* + * failedVerification is true if any enforced certificate checks have failed. + * Connections that have not yet tried to verify, or are using acceptable + * exceptions will all return false. + */ + [infallible] readonly attribute boolean failedVerification; + + /* + * esniTxt is a string that consists of the concatenated _esni. TXT records. + * This is a base64 encoded ESNIKeys structure. + */ + attribute ACString esniTxt; + + /* + * echConfig is defined for conveying the ECH configuration. + * This is encoded in base64. + */ + attribute ACString echConfig; + + /** + * The id used to uniquely identify the connection to the peer. + */ + readonly attribute ACString peerId; + + /** + * The echConfig that should be used to retry for the connection setup. + */ + readonly attribute ACString retryEchConfig; +}; + diff --git a/netwerk/socket/nsISocketProvider.idl b/netwerk/socket/nsISocketProvider.idl new file mode 100644 index 0000000000..bbc13a7778 --- /dev/null +++ b/netwerk/socket/nsISocketProvider.idl @@ -0,0 +1,114 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIProxyInfo; +[ptr] native PRFileDescStar(struct PRFileDesc); +native OriginAttributes(mozilla::OriginAttributes); +[ref] native const_OriginAttributesRef(const mozilla::OriginAttributes); + +%{ C++ +#include "mozilla/BasePrincipal.h" +%} + +/** + * nsISocketProvider + */ +[scriptable, uuid(508d5469-9e1e-4a08-b5b0-7cfebba1e51a)] +interface nsISocketProvider : nsISupports +{ + /** + * newSocket + * + * @param aFamily + * The address family for this socket (PR_AF_INET or PR_AF_INET6). + * @param aHost + * The origin hostname for this connection. + * @param aPort + * The origin port for this connection. + * @param aProxyHost + * If non-null, the proxy hostname for this connection. + * @param aProxyPort + * The proxy port for this connection. + * @param aFlags + * Control flags that govern this connection (see below.) + * @param aTlsFlags + * An opaque flags for non-standard behavior of the TLS system. + * It is unlikely this will need to be set outside of telemetry + * studies relating to the TLS implementation. + * @param aFileDesc + * The resulting PRFileDesc. + * @param aSecurityInfo + * Any security info that should be associated with aFileDesc. This + * object typically implements nsITransportSecurityInfo. + */ + [noscript] + void newSocket(in long aFamily, + in string aHost, + in long aPort, + in nsIProxyInfo aProxy, + in const_OriginAttributesRef aOriginAttributes, + in unsigned long aFlags, + in unsigned long aTlsFlags, + out PRFileDescStar aFileDesc, + out nsISupports aSecurityInfo); + + /** + * addToSocket + * + * This function is called to allow the socket provider to layer a + * PRFileDesc on top of another PRFileDesc. For example, SSL via a SOCKS + * proxy. + * + * Parameters are the same as newSocket with the exception of aFileDesc, + * which is an in-param instead. + */ + [noscript] + void addToSocket(in long aFamily, + in string aHost, + in long aPort, + in nsIProxyInfo aProxy, + in const_OriginAttributesRef aOriginAttributes, + in unsigned long aFlags, + in unsigned long aTlsFlags, + in PRFileDescStar aFileDesc, + out nsISupports aSecurityInfo); + + /** + * PROXY_RESOLVES_HOST + * + * This flag is set if the proxy is to perform hostname resolution instead + * of the client. When set, the hostname parameter passed when in this + * interface will be used instead of the address structure passed for a + * later connect et al. request. + */ + const long PROXY_RESOLVES_HOST = 1 << 0; + + /** + * When setting this flag, the socket will not apply any + * credentials when establishing a connection. For example, + * an SSL connection would not send any client-certificates + * if this flag is set. + */ + const long ANONYMOUS_CONNECT = 1 << 1; + + /** + * If set, indicates that the connection was initiated from a source + * defined as being private in the sense of Private Browsing. Generally, + * there should be no state shared between connections that are private + * and those that are not; it is OK for multiple private connections + * to share state with each other, and it is OK for multiple non-private + * connections to share state with each other. + */ + const unsigned long NO_PERMANENT_STORAGE = 1 << 2; + + /** + * If set, do not use newer protocol features that might have interop problems + * on the Internet. Intended only for use with critical infra like the updater. + * default is false. + */ + const unsigned long BE_CONSERVATIVE = 1 << 3; +}; diff --git a/netwerk/socket/nsISocketProviderService.idl b/netwerk/socket/nsISocketProviderService.idl new file mode 100644 index 0000000000..db38d299a5 --- /dev/null +++ b/netwerk/socket/nsISocketProviderService.idl @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsISocketProvider; + +/** + * nsISocketProviderService + * + * Provides a mapping between a socket type and its associated socket provider + * instance. One could also use the service manager directly. + */ +[scriptable, uuid(8f8a23d0-5472-11d3-bbc8-0000861d1237)] +interface nsISocketProviderService : nsISupports +{ + nsISocketProvider getSocketProvider(in string socketType); +}; diff --git a/netwerk/socket/nsITransportSecurityInfo.idl b/netwerk/socket/nsITransportSecurityInfo.idl new file mode 100644 index 0000000000..b5722d328d --- /dev/null +++ b/netwerk/socket/nsITransportSecurityInfo.idl @@ -0,0 +1,114 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIX509Cert; + +%{ C++ +namespace IPC { + class Message; +} +class PickleIterator; +%} + +[ptr] native IpcMessagePtr(IPC::Message); +[ptr] native PickleIteratorPtr(PickleIterator); + +[builtinclass, scriptable, uuid(216112d3-28bc-4671-b057-f98cc09ba1ea)] +interface nsITransportSecurityInfo : nsISupports { + readonly attribute unsigned long securityState; + readonly attribute long errorCode; // PRErrorCode + // errorCode as string (e.g. "SEC_ERROR_UNKNOWN_ISSUER") + readonly attribute AString errorCodeString; + + /** + * The following parameters are only valid after the TLS handshake + * has completed. Check securityState first. + */ + + /** + * If certificate verification failed, this will be the peer certificate + * chain provided in the handshake, so it can be used for error reporting. + * If verification succeeded, this will be empty. + */ + readonly attribute Array<nsIX509Cert> failedCertChain; + + readonly attribute nsIX509Cert serverCert; + readonly attribute Array<nsIX509Cert> succeededCertChain; + + [must_use] + readonly attribute ACString cipherName; + [must_use] + readonly attribute unsigned long keyLength; + [must_use] + readonly attribute unsigned long secretKeyLength; + [must_use] + readonly attribute ACString keaGroupName; + [must_use] + readonly attribute ACString signatureSchemeName; + + const short SSL_VERSION_3 = 0; + const short TLS_VERSION_1 = 1; + const short TLS_VERSION_1_1 = 2; + const short TLS_VERSION_1_2 = 3; + const short TLS_VERSION_1_3 = 4; + [must_use] + readonly attribute unsigned short protocolVersion; + + const short CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE = 0; + const short CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT = 5; + const short CERTIFICATE_TRANSPARENCY_POLICY_NOT_ENOUGH_SCTS = 6; + const short CERTIFICATE_TRANSPARENCY_POLICY_NOT_DIVERSE_SCTS = 7; + [must_use] + readonly attribute unsigned short certificateTransparencyStatus; + + [must_use] + readonly attribute boolean isAcceptedEch; + [must_use] + readonly attribute boolean isDelegatedCredential; + [must_use] + readonly attribute boolean isDomainMismatch; + [must_use] + readonly attribute boolean isNotValidAtThisTime; + + [must_use] + readonly attribute boolean isUntrusted; + + /** + * True only if (and after) serverCert was successfully validated as + * Extended Validation (EV). + */ + [must_use] + readonly attribute boolean isExtendedValidation; + + [notxpcom, noscript] + void SerializeToIPC(in IpcMessagePtr aMsg); + + [notxpcom, noscript] + bool DeserializeFromIPC([const] in IpcMessagePtr aMsg, in PickleIteratorPtr aIter); + + /* negotiatedNPN is '' if no NPN list was provided by the client, + * or if the server did not select any protocol choice from that + * list. That also includes the case where the server does not + * implement NPN. + * + * If negotiatedNPN is read before NPN has progressed to the point + * where this information is available NS_ERROR_NOT_CONNECTED is + * raised. + */ + readonly attribute ACString negotiatedNPN; + + /** + * True iff the connection was resumed using the resumption token. + */ + readonly attribute boolean resumed; + + /** + * True iff the succeededCertChain is built in root. + */ + attribute boolean isBuiltCertChainRootBuiltInRoot; +}; diff --git a/netwerk/socket/nsNamedPipeIOLayer.cpp b/netwerk/socket/nsNamedPipeIOLayer.cpp new file mode 100644 index 0000000000..3fb31a3a45 --- /dev/null +++ b/netwerk/socket/nsNamedPipeIOLayer.cpp @@ -0,0 +1,864 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsNamedPipeIOLayer.h" + +#include <algorithm> +#include <utility> + +#include "mozilla/Atomics.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Logging.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Unused.h" +#include "mozilla/net/DNS.h" +#include "nsISupportsImpl.h" +#include "nsNamedPipeService.h" +#include "nsNativeCharsetUtils.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" +#include "nsSocketTransportService2.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "nspr.h" +#include "private/pprio.h" + +namespace mozilla { +namespace net { + +static mozilla::LazyLogModule gNamedPipeLog("NamedPipeWin"); +#define LOG_NPIO_DEBUG(...) \ + MOZ_LOG(gNamedPipeLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +#define LOG_NPIO_ERROR(...) \ + MOZ_LOG(gNamedPipeLog, mozilla::LogLevel::Error, (__VA_ARGS__)) + +PRDescIdentity nsNamedPipeLayerIdentity; +static PRIOMethods nsNamedPipeLayerMethods; + +class NamedPipeInfo final : public nsINamedPipeDataObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSINAMEDPIPEDATAOBSERVER + + explicit NamedPipeInfo(); + + nsresult Connect(const nsAString& aPath); + nsresult Disconnect(); + + /** + * Both blocking/non-blocking mode are supported in this class. + * The default mode is non-blocking mode, however, the client may change its + * mode to blocking mode during hand-shaking (e.g. nsSOCKSSocketInfo). + * + * In non-blocking mode, |Read| and |Write| should be called by clients only + * when |GetPollFlags| reports data availability. That is, the client calls + * |GetPollFlags| with |PR_POLL_READ| and/or |PR_POLL_WRITE| set, and + * according to the flags that set, |GetPollFlags| will check buffers status + * and decide corresponding actions: + * + * ------------------------------------------------------------------- + * | | data in buffer | empty buffer | + * |---------------+-------------------------+-----------------------| + * | PR_POLL_READ | out: PR_POLL_READ | DoRead/DoReadContinue | + * |---------------+-------------------------+-----------------------| + * | PR_POLL_WRITE | DoWrite/DoWriteContinue | out: PR_POLL_WRITE | + * ------------------------------------------+------------------------ + * + * |DoRead| and |DoWrite| initiate read/write operations asynchronously, and + * the |DoReadContinue| and |DoWriteContinue| are used to check the amount + * of the data are read/written to/from buffers. + * + * The output parameter and the return value of |GetPollFlags| are identical + * because we don't rely on the low-level select function to wait for data + * availability, we instead use nsNamedPipeService to poll I/O completeness. + * + * When client get |PR_POLL_READ| or |PR_POLL_WRITE| from |GetPollFlags|, + * they are able to use |Read| or |Write| to access the data in the buffer, + * and this is supposed to be very fast because no network traffic is + * involved. + * + * In blocking mode, the flow is quite similar to non-blocking mode, but + * |DoReadContinue| and |DoWriteContinue| are never been used since the + * operations are done synchronously, which could lead to slow responses. + */ + int32_t Read(void* aBuffer, int32_t aSize); + int32_t Write(const void* aBuffer, int32_t aSize); + + // Like Read, but doesn't remove data in internal buffer. + uint32_t Peek(void* aBuffer, int32_t aSize); + + // Number of bytes available to read in internal buffer. + int32_t Available() const; + + // Flush write buffer + // + // @return whether the buffer has been flushed + bool Sync(uint32_t aTimeout); + void SetNonblocking(bool nonblocking); + + bool IsConnected() const; + bool IsNonblocking() const; + HANDLE GetHandle() const; + + // Initiate and check current status for read/write operations. + int16_t GetPollFlags(int16_t aInFlags, int16_t* aOutFlags); + + private: + virtual ~NamedPipeInfo(); + + /** + * DoRead/DoWrite starts a read/write call synchronously or asynchronously + * depending on |mNonblocking|. In blocking mode, they return when the action + * has been done and in non-blocking mode it returns the number of bytes that + * were read/written if the operation is done immediately. If it takes some + * time to finish the operation, zero is returned and + * DoReadContinue/DoWriteContinue must be called to get async I/O result. + */ + int32_t DoRead(); + int32_t DoReadContinue(); + int32_t DoWrite(); + int32_t DoWriteContinue(); + + /** + * There was a write size limitation of named pipe, + * see https://support.microsoft.com/en-us/kb/119218 for more information. + * The limitation no longer exists, so feel free to change the value. + */ + static const uint32_t kBufferSize = 65536; + + nsCOMPtr<nsINamedPipeService> mNamedPipeService; + + HANDLE mPipe; // the handle to the named pipe. + OVERLAPPED mReadOverlapped; // used for asynchronous read operations. + OVERLAPPED mWriteOverlapped; // used for asynchronous write operations. + + uint8_t mReadBuffer[kBufferSize]; // octets read from pipe. + + /** + * These indicates the [begin, end) position of the data in the buffer. + */ + DWORD mReadBegin; + DWORD mReadEnd; + + bool mHasPendingRead; // previous asynchronous read is not finished yet. + + uint8_t mWriteBuffer[kBufferSize]; // octets to be written to pipe. + + /** + * These indicates the [begin, end) position of the data in the buffer. + */ + DWORD mWriteBegin; // how many bytes are already written. + DWORD mWriteEnd; // valid amount of data in the buffer. + + bool mHasPendingWrite; // previous asynchronous write is not finished yet. + + /** + * current blocking mode is non-blocking or not, accessed only in socket + * thread. + */ + bool mNonblocking; + + Atomic<DWORD> mErrorCode; // error code from Named Pipe Service. +}; + +NS_IMPL_ISUPPORTS(NamedPipeInfo, nsINamedPipeDataObserver) + +NamedPipeInfo::NamedPipeInfo() + : mNamedPipeService(NamedPipeService::GetOrCreate()), + mPipe(INVALID_HANDLE_VALUE), + mReadBegin(0), + mReadEnd(0), + mHasPendingRead(false), + mWriteBegin(0), + mWriteEnd(0), + mHasPendingWrite(false), + mNonblocking(true), + mErrorCode(0) { + MOZ_ASSERT(mNamedPipeService); + + ZeroMemory(&mReadOverlapped, sizeof(OVERLAPPED)); + ZeroMemory(&mWriteOverlapped, sizeof(OVERLAPPED)); +} + +NamedPipeInfo::~NamedPipeInfo() { MOZ_ASSERT(!mPipe); } + +// nsINamedPipeDataObserver + +NS_IMETHODIMP +NamedPipeInfo::OnDataAvailable(uint32_t aBytesTransferred, void* aOverlapped) { + DebugOnly<bool> isOnPipeServiceThread; + MOZ_ASSERT(NS_SUCCEEDED(mNamedPipeService->IsOnCurrentThread( + &isOnPipeServiceThread)) && + isOnPipeServiceThread); + + if (aOverlapped == &mReadOverlapped) { + LOG_NPIO_DEBUG("[%s] %p read %d bytes", __func__, this, aBytesTransferred); + } else if (aOverlapped == &mWriteOverlapped) { + LOG_NPIO_DEBUG("[%s] %p write %d bytes", __func__, this, aBytesTransferred); + } else { + MOZ_ASSERT(false, "invalid callback"); + mErrorCode = ERROR_INVALID_DATA; + return NS_ERROR_FAILURE; + } + + mErrorCode = ERROR_SUCCESS; + + // dispatch an empty event to trigger STS thread + gSocketTransportService->Dispatch( + NS_NewRunnableFunction("NamedPipeInfo::OnDataAvailable", [] {}), + NS_DISPATCH_NORMAL); + + return NS_OK; +} + +NS_IMETHODIMP +NamedPipeInfo::OnError(uint32_t aError, void* aOverlapped) { + DebugOnly<bool> isOnPipeServiceThread; + MOZ_ASSERT(NS_SUCCEEDED(mNamedPipeService->IsOnCurrentThread( + &isOnPipeServiceThread)) && + isOnPipeServiceThread); + + LOG_NPIO_ERROR("[%s] error code=%d", __func__, aError); + mErrorCode = aError; + + // dispatch an empty event to trigger STS thread + gSocketTransportService->Dispatch( + NS_NewRunnableFunction("NamedPipeInfo::OnError", [] {}), + NS_DISPATCH_NORMAL); + + return NS_OK; +} + +// Named pipe operations + +nsresult NamedPipeInfo::Connect(const nsAString& aPath) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + HANDLE pipe = + CreateFileW(PromiseFlatString(aPath).get(), GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, + FILE_FLAG_OVERLAPPED, nullptr); + + if (pipe == INVALID_HANDLE_VALUE) { + LOG_NPIO_ERROR("[%p] CreateFile error (%d)", this, GetLastError()); + return NS_ERROR_FAILURE; + } + + DWORD pipeMode = PIPE_READMODE_MESSAGE; + if (!SetNamedPipeHandleState(pipe, &pipeMode, nullptr, nullptr)) { + LOG_NPIO_ERROR("[%p] SetNamedPipeHandleState error (%d)", this, + GetLastError()); + CloseHandle(pipe); + return NS_ERROR_FAILURE; + } + + nsresult rv = mNamedPipeService->AddDataObserver(pipe, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + CloseHandle(pipe); + return rv; + } + + HANDLE readEvent = CreateEventA(nullptr, TRUE, TRUE, "NamedPipeRead"); + if (NS_WARN_IF(!readEvent || readEvent == INVALID_HANDLE_VALUE)) { + CloseHandle(pipe); + return NS_ERROR_FAILURE; + } + + HANDLE writeEvent = CreateEventA(nullptr, TRUE, TRUE, "NamedPipeWrite"); + if (NS_WARN_IF(!writeEvent || writeEvent == INVALID_HANDLE_VALUE)) { + CloseHandle(pipe); + CloseHandle(readEvent); + return NS_ERROR_FAILURE; + } + + mPipe = pipe; + mReadOverlapped.hEvent = readEvent; + mWriteOverlapped.hEvent = writeEvent; + return NS_OK; +} + +nsresult NamedPipeInfo::Disconnect() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsresult rv = mNamedPipeService->RemoveDataObserver(mPipe, this); + Unused << NS_WARN_IF(NS_FAILED(rv)); + + mPipe = nullptr; + + if (mReadOverlapped.hEvent && + mReadOverlapped.hEvent != INVALID_HANDLE_VALUE) { + CloseHandle(mReadOverlapped.hEvent); + mReadOverlapped.hEvent = nullptr; + } + + if (mWriteOverlapped.hEvent && + mWriteOverlapped.hEvent != INVALID_HANDLE_VALUE) { + CloseHandle(mWriteOverlapped.hEvent); + mWriteOverlapped.hEvent = nullptr; + } + + return rv; +} + +int32_t NamedPipeInfo::Read(void* aBuffer, int32_t aSize) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + int32_t bytesRead = Peek(aBuffer, aSize); + + if (bytesRead > 0) { + mReadBegin += bytesRead; + } + + return bytesRead; +} + +int32_t NamedPipeInfo::Write(const void* aBuffer, int32_t aSize) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mWriteBegin <= mWriteEnd); + + if (!IsConnected()) { + // pipe unconnected + PR_SetError(PR_NOT_CONNECTED_ERROR, 0); + return -1; + } + + if (mWriteBegin == mWriteEnd) { + mWriteBegin = mWriteEnd = 0; + } + + int32_t bytesToWrite = + std::min<int32_t>(aSize, sizeof(mWriteBuffer) - mWriteEnd); + MOZ_ASSERT(bytesToWrite >= 0); + + if (bytesToWrite == 0) { + PR_SetError(IsNonblocking() ? PR_WOULD_BLOCK_ERROR : PR_IO_PENDING_ERROR, + 0); + return -1; + } + + memcpy(&mWriteBuffer[mWriteEnd], aBuffer, bytesToWrite); + mWriteEnd += bytesToWrite; + + /** + * Triggers internal write operation by calling |GetPollFlags|. + * This is required for callers that use blocking I/O because they don't call + * |GetPollFlags| to write data, but this also works for non-blocking I/O. + */ + int16_t outFlag; + GetPollFlags(PR_POLL_WRITE, &outFlag); + + return bytesToWrite; +} + +uint32_t NamedPipeInfo::Peek(void* aBuffer, int32_t aSize) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mReadBegin <= mReadEnd); + + if (!IsConnected()) { + // pipe unconnected + PR_SetError(PR_NOT_CONNECTED_ERROR, 0); + return -1; + } + + /** + * If there's nothing in the read buffer, try to trigger internal read + * operation by calling |GetPollFlags|. This is required for callers that + * use blocking I/O because they don't call |GetPollFlags| to read data, + * but this also works for non-blocking I/O. + */ + if (!Available()) { + int16_t outFlag; + GetPollFlags(PR_POLL_READ, &outFlag); + + if (!(outFlag & PR_POLL_READ)) { + PR_SetError(IsNonblocking() ? PR_WOULD_BLOCK_ERROR : PR_IO_PENDING_ERROR, + 0); + return -1; + } + } + + // Available() can't return more than what fits to the buffer at the read + // offset. + int32_t bytesRead = std::min<int32_t>(aSize, Available()); + MOZ_ASSERT(bytesRead >= 0); + MOZ_ASSERT(mReadBegin + bytesRead <= mReadEnd); + memcpy(aBuffer, &mReadBuffer[mReadBegin], bytesRead); + return bytesRead; +} + +int32_t NamedPipeInfo::Available() const { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mReadBegin <= mReadEnd); + MOZ_ASSERT(mReadEnd - mReadBegin <= 0x7FFFFFFF); // no more than int32_max + return mReadEnd - mReadBegin; +} + +bool NamedPipeInfo::Sync(uint32_t aTimeout) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (!mHasPendingWrite) { + return true; + } + return WaitForSingleObject(mWriteOverlapped.hEvent, aTimeout) == + WAIT_OBJECT_0; +} + +void NamedPipeInfo::SetNonblocking(bool nonblocking) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + mNonblocking = nonblocking; +} + +bool NamedPipeInfo::IsConnected() const { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + return mPipe && mPipe != INVALID_HANDLE_VALUE; +} + +bool NamedPipeInfo::IsNonblocking() const { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + return mNonblocking; +} + +HANDLE +NamedPipeInfo::GetHandle() const { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + return mPipe; +} + +int16_t NamedPipeInfo::GetPollFlags(int16_t aInFlags, int16_t* aOutFlags) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + *aOutFlags = 0; + + if (aInFlags & PR_POLL_READ) { + int32_t bytesToRead = 0; + if (mReadBegin < mReadEnd) { // data in buffer and is ready to be read + bytesToRead = Available(); + } else if (mHasPendingRead) { // nonblocking I/O and has pending task + bytesToRead = DoReadContinue(); + } else { // read bufer is empty. + bytesToRead = DoRead(); + } + + if (bytesToRead > 0) { + *aOutFlags |= PR_POLL_READ; + } else if (bytesToRead < 0) { + *aOutFlags |= PR_POLL_ERR; + } + } + + if (aInFlags & PR_POLL_WRITE) { + int32_t bytesWritten = 0; + if (mHasPendingWrite) { // nonblocking I/O and has pending task. + bytesWritten = DoWriteContinue(); + } else if (mWriteBegin < mWriteEnd) { // data in buffer, ready to write + bytesWritten = DoWrite(); + } else { // write buffer is empty. + *aOutFlags |= PR_POLL_WRITE; + } + + if (bytesWritten < 0) { + *aOutFlags |= PR_POLL_ERR; + } else if (bytesWritten && !mHasPendingWrite && mWriteBegin == mWriteEnd) { + *aOutFlags |= PR_POLL_WRITE; + } + } + + return *aOutFlags; +} + +// @return: data has been read and is available +int32_t NamedPipeInfo::DoRead() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!mHasPendingRead); + MOZ_ASSERT(mReadBegin == mReadEnd); // the buffer should be empty + + mReadBegin = 0; + mReadEnd = 0; + + BOOL success = ReadFile(mPipe, mReadBuffer, sizeof(mReadBuffer), &mReadEnd, + IsNonblocking() ? &mReadOverlapped : nullptr); + + if (success) { + LOG_NPIO_DEBUG("[%s][%p] %d bytes read", __func__, this, mReadEnd); + return mReadEnd; + } + + switch (GetLastError()) { + case ERROR_MORE_DATA: // has more data to read + mHasPendingRead = true; + return DoReadContinue(); + + case ERROR_IO_PENDING: // read is pending + mHasPendingRead = true; + break; + + default: + LOG_NPIO_ERROR("[%s] ReadFile failed (%d)", __func__, GetLastError()); + Disconnect(); + PR_SetError(PR_IO_ERROR, 0); + return -1; + } + + return 0; +} + +int32_t NamedPipeInfo::DoReadContinue() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mHasPendingRead); + MOZ_ASSERT(mReadBegin == 0 && mReadEnd == 0); + + BOOL success; + success = GetOverlappedResult(mPipe, &mReadOverlapped, &mReadEnd, FALSE); + if (success) { + mHasPendingRead = false; + if (mReadEnd == 0) { + Disconnect(); + PR_SetError(PR_NOT_CONNECTED_ERROR, 0); + return -1; + } + + LOG_NPIO_DEBUG("[%s][%p] %d bytes read", __func__, this, mReadEnd); + return mReadEnd; + } + + switch (GetLastError()) { + case ERROR_MORE_DATA: + mHasPendingRead = false; + LOG_NPIO_DEBUG("[%s][%p] %d bytes read", __func__, this, mReadEnd); + return mReadEnd; + case ERROR_IO_INCOMPLETE: // still in progress + break; + default: + LOG_NPIO_ERROR("[%s]: GetOverlappedResult failed (%d)", __func__, + GetLastError()); + Disconnect(); + PR_SetError(PR_IO_ERROR, 0); + return -1; + } + + return 0; +} + +int32_t NamedPipeInfo::DoWrite() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!mHasPendingWrite); + MOZ_ASSERT(mWriteBegin < mWriteEnd); + + DWORD bytesWritten = 0; + BOOL success = + WriteFile(mPipe, &mWriteBuffer[mWriteBegin], mWriteEnd - mWriteBegin, + &bytesWritten, IsNonblocking() ? &mWriteOverlapped : nullptr); + + if (success) { + mWriteBegin += bytesWritten; + LOG_NPIO_DEBUG("[%s][%p] %d bytes written", __func__, this, bytesWritten); + return bytesWritten; + } + + if (GetLastError() != ERROR_IO_PENDING) { + LOG_NPIO_ERROR("[%s] WriteFile failed (%d)", __func__, GetLastError()); + Disconnect(); + PR_SetError(PR_IO_ERROR, 0); + return -1; + } + + mHasPendingWrite = true; + + return 0; +} + +int32_t NamedPipeInfo::DoWriteContinue() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mHasPendingWrite); + + DWORD bytesWritten = 0; + BOOL success = + GetOverlappedResult(mPipe, &mWriteOverlapped, &bytesWritten, FALSE); + + if (!success) { + if (GetLastError() == ERROR_IO_INCOMPLETE) { + // still in progress + return 0; + } + + LOG_NPIO_ERROR("[%s] GetOverlappedResult failed (%d)", __func__, + GetLastError()); + Disconnect(); + PR_SetError(PR_IO_ERROR, 0); + return -1; + } + + mHasPendingWrite = false; + mWriteBegin += bytesWritten; + LOG_NPIO_DEBUG("[%s][%p] %d bytes written", __func__, this, bytesWritten); + return bytesWritten; +} + +static inline NamedPipeInfo* GetNamedPipeInfo(PRFileDesc* aFd) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_DIAGNOSTIC_ASSERT(aFd); + MOZ_DIAGNOSTIC_ASSERT(aFd->secret); + MOZ_DIAGNOSTIC_ASSERT(PR_GetLayersIdentity(aFd) == nsNamedPipeLayerIdentity); + + if (!aFd || !aFd->secret || + PR_GetLayersIdentity(aFd) != nsNamedPipeLayerIdentity) { + LOG_NPIO_ERROR("cannot get named pipe info"); + return nullptr; + } + + return reinterpret_cast<NamedPipeInfo*>(aFd->secret); +} + +static PRStatus nsNamedPipeConnect(PRFileDesc* aFd, const PRNetAddr* aAddr, + PRIntervalTime aTimeout) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + NamedPipeInfo* info = GetNamedPipeInfo(aFd); + if (!info) { + PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); + return PR_FAILURE; + } + + nsAutoString path; + if (NS_FAILED(NS_CopyNativeToUnicode(nsDependentCString(aAddr->local.path), + path))) { + PR_SetError(PR_OUT_OF_MEMORY_ERROR, 0); + return PR_FAILURE; + } + if (NS_WARN_IF(NS_FAILED(info->Connect(path)))) { + return PR_FAILURE; + } + + return PR_SUCCESS; +} + +static PRStatus nsNamedPipeConnectContinue(PRFileDesc* aFd, PRInt16 aOutFlags) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + return PR_SUCCESS; +} + +static PRStatus nsNamedPipeClose(PRFileDesc* aFd) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (aFd->secret && PR_GetLayersIdentity(aFd) == nsNamedPipeLayerIdentity) { + RefPtr<NamedPipeInfo> info = dont_AddRef(GetNamedPipeInfo(aFd)); + info->Disconnect(); + aFd->secret = nullptr; + aFd->identity = PR_INVALID_IO_LAYER; + } + + MOZ_ASSERT(!aFd->lower); + PR_Free(aFd); // PRFileDescs are allocated with PR_Malloc(). + + return PR_SUCCESS; +} + +static PRInt32 nsNamedPipeSend(PRFileDesc* aFd, const void* aBuffer, + PRInt32 aAmount, PRIntn aFlags, + PRIntervalTime aTimeout) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + Unused << aFlags; + Unused << aTimeout; + + NamedPipeInfo* info = GetNamedPipeInfo(aFd); + if (!info) { + PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); + return -1; + } + return info->Write(aBuffer, aAmount); +} + +static PRInt32 nsNamedPipeRecv(PRFileDesc* aFd, void* aBuffer, PRInt32 aAmount, + PRIntn aFlags, PRIntervalTime aTimeout) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + Unused << aTimeout; + + NamedPipeInfo* info = GetNamedPipeInfo(aFd); + if (!info) { + PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); + return -1; + } + + if (aFlags) { + if (aFlags != PR_MSG_PEEK) { + PR_SetError(PR_UNKNOWN_ERROR, 0); + return -1; + } + return info->Peek(aBuffer, aAmount); + } + + return info->Read(aBuffer, aAmount); +} + +static inline PRInt32 nsNamedPipeRead(PRFileDesc* aFd, void* aBuffer, + PRInt32 aAmount) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + NamedPipeInfo* info = GetNamedPipeInfo(aFd); + if (!info) { + PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); + return -1; + } + return info->Read(aBuffer, aAmount); +} + +static inline PRInt32 nsNamedPipeWrite(PRFileDesc* aFd, const void* aBuffer, + PRInt32 aAmount) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + NamedPipeInfo* info = GetNamedPipeInfo(aFd); + if (!info) { + PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); + return -1; + } + return info->Write(aBuffer, aAmount); +} + +static PRInt32 nsNamedPipeAvailable(PRFileDesc* aFd) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + NamedPipeInfo* info = GetNamedPipeInfo(aFd); + if (!info) { + PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); + return -1; + } + return static_cast<PRInt32>(info->Available()); +} + +static PRInt64 nsNamedPipeAvailable64(PRFileDesc* aFd) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + NamedPipeInfo* info = GetNamedPipeInfo(aFd); + if (!info) { + PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); + return -1; + } + return static_cast<PRInt64>(info->Available()); +} + +static PRStatus nsNamedPipeSync(PRFileDesc* aFd) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + NamedPipeInfo* info = GetNamedPipeInfo(aFd); + if (!info) { + PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); + return PR_FAILURE; + } + return info->Sync(0) ? PR_SUCCESS : PR_FAILURE; +} + +static PRInt16 nsNamedPipePoll(PRFileDesc* aFd, PRInt16 aInFlags, + PRInt16* aOutFlags) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + NamedPipeInfo* info = GetNamedPipeInfo(aFd); + if (!info) { + PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); + return 0; + } + return info->GetPollFlags(aInFlags, aOutFlags); +} + +// FIXME: remove socket option functions? +static PRStatus nsNamedPipeGetSocketOption(PRFileDesc* aFd, + PRSocketOptionData* aData) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + MOZ_ASSERT(aFd); + MOZ_ASSERT(aData); + + switch (aData->option) { + case PR_SockOpt_Nonblocking: + aData->value.non_blocking = + GetNamedPipeInfo(aFd)->IsNonblocking() ? PR_TRUE : PR_FALSE; + break; + case PR_SockOpt_Keepalive: + aData->value.keep_alive = PR_TRUE; + break; + case PR_SockOpt_NoDelay: + aData->value.no_delay = PR_TRUE; + break; + default: + PR_SetError(PR_INVALID_METHOD_ERROR, 0); + return PR_FAILURE; + } + + return PR_SUCCESS; +} + +static PRStatus nsNamedPipeSetSocketOption(PRFileDesc* aFd, + const PRSocketOptionData* aData) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + MOZ_ASSERT(aFd); + MOZ_ASSERT(aData); + + switch (aData->option) { + case PR_SockOpt_Nonblocking: + GetNamedPipeInfo(aFd)->SetNonblocking(aData->value.non_blocking); + break; + case PR_SockOpt_Keepalive: + case PR_SockOpt_NoDelay: + break; + default: + PR_SetError(PR_INVALID_METHOD_ERROR, 0); + return PR_FAILURE; + } + + return PR_SUCCESS; +} + +static void Initialize() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + static bool initialized = false; + if (initialized) { + return; + } + + nsNamedPipeLayerIdentity = PR_GetUniqueIdentity("Named Pipe layer"); + nsNamedPipeLayerMethods = *PR_GetDefaultIOMethods(); + nsNamedPipeLayerMethods.close = nsNamedPipeClose; + nsNamedPipeLayerMethods.read = nsNamedPipeRead; + nsNamedPipeLayerMethods.write = nsNamedPipeWrite; + nsNamedPipeLayerMethods.available = nsNamedPipeAvailable; + nsNamedPipeLayerMethods.available64 = nsNamedPipeAvailable64; + nsNamedPipeLayerMethods.fsync = nsNamedPipeSync; + nsNamedPipeLayerMethods.connect = nsNamedPipeConnect; + nsNamedPipeLayerMethods.recv = nsNamedPipeRecv; + nsNamedPipeLayerMethods.send = nsNamedPipeSend; + nsNamedPipeLayerMethods.poll = nsNamedPipePoll; + nsNamedPipeLayerMethods.getsocketoption = nsNamedPipeGetSocketOption; + nsNamedPipeLayerMethods.setsocketoption = nsNamedPipeSetSocketOption; + nsNamedPipeLayerMethods.connectcontinue = nsNamedPipeConnectContinue; + + initialized = true; +} + +bool IsNamedPipePath(const nsACString& aPath) { + return StringBeginsWith(aPath, "\\\\.\\pipe\\"_ns); +} + +PRFileDesc* CreateNamedPipeLayer() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + Initialize(); + + PRFileDesc* layer = + PR_CreateIOLayerStub(nsNamedPipeLayerIdentity, &nsNamedPipeLayerMethods); + if (NS_WARN_IF(!layer)) { + LOG_NPIO_ERROR("CreateNamedPipeLayer() failed."); + return nullptr; + } + + RefPtr<NamedPipeInfo> info = new NamedPipeInfo(); + layer->secret = reinterpret_cast<PRFilePrivate*>(info.forget().take()); + + return layer; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/socket/nsNamedPipeIOLayer.h b/netwerk/socket/nsNamedPipeIOLayer.h new file mode 100644 index 0000000000..87598324b6 --- /dev/null +++ b/netwerk/socket/nsNamedPipeIOLayer.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_netwerk_socket_nsNamedPipeIOLayer_h +#define mozilla_netwerk_socket_nsNamedPipeIOLayer_h + +#include "nscore.h" +#include "nsStringFwd.h" +#include "prio.h" + +namespace mozilla { +namespace net { + +bool IsNamedPipePath(const nsACString& aPath); +PRFileDesc* CreateNamedPipeLayer(); + +extern PRDescIdentity nsNamedPipeLayerIdentity; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_netwerk_socket_nsNamedPipeIOLayer_h diff --git a/netwerk/socket/nsNamedPipeService.cpp b/netwerk/socket/nsNamedPipeService.cpp new file mode 100644 index 0000000000..06be8562a8 --- /dev/null +++ b/netwerk/socket/nsNamedPipeService.cpp @@ -0,0 +1,318 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Services.h" +#include "nsCOMPtr.h" +#include "nsIObserverService.h" +#include "nsNamedPipeService.h" +#include "nsNetCID.h" +#include "nsThreadUtils.h" +#include "mozilla/ClearOnShutdown.h" + +namespace mozilla { +namespace net { + +static mozilla::LazyLogModule gNamedPipeServiceLog("NamedPipeWin"); +#define LOG_NPS_DEBUG(...) \ + MOZ_LOG(gNamedPipeServiceLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +#define LOG_NPS_ERROR(...) \ + MOZ_LOG(gNamedPipeServiceLog, mozilla::LogLevel::Error, (__VA_ARGS__)) + +StaticRefPtr<NamedPipeService> NamedPipeService::gSingleton; + +NS_IMPL_ISUPPORTS(NamedPipeService, nsINamedPipeService, nsIObserver, + nsIRunnable) + +NamedPipeService::NamedPipeService() + : mIocp(nullptr), mIsShutdown(false), mLock("NamedPipeServiceLock") {} + +nsresult NamedPipeService::Init() { + MOZ_ASSERT(!mIsShutdown); + + nsresult rv; + + // nsIObserverService must be accessed in main thread. + // register shutdown event to stop NamedPipeSrv thread. + nsCOMPtr<nsIObserver> self(this); + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "NamedPipeService::Init", [self = std::move(self)]() -> void { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> svc = + mozilla::services::GetObserverService(); + + if (NS_WARN_IF(!svc)) { + return; + } + + if (NS_WARN_IF(NS_FAILED(svc->AddObserver( + self, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false)))) { + return; + } + }); + + if (NS_IsMainThread()) { + rv = r->Run(); + } else { + rv = NS_DispatchToMainThread(r); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 1); + if (NS_WARN_IF(!mIocp || mIocp == INVALID_HANDLE_VALUE)) { + Shutdown(); + return NS_ERROR_FAILURE; + } + + rv = NS_NewNamedThread("NamedPipeSrv", getter_AddRefs(mThread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + Shutdown(); + return rv; + } + + return NS_OK; +} + +// static +already_AddRefed<nsINamedPipeService> NamedPipeService::GetOrCreate() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<NamedPipeService> inst; + if (gSingleton) { + inst = gSingleton; + } else { + inst = new NamedPipeService(); + nsresult rv = inst->Init(); + NS_ENSURE_SUCCESS(rv, nullptr); + gSingleton = inst; + ClearOnShutdown(&gSingleton); + } + + return inst.forget(); +} + +void NamedPipeService::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + + // remove observer + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + } + + // stop thread + if (mThread && !mIsShutdown) { + mIsShutdown = true; + + // invoke ERROR_ABANDONED_WAIT_0 to |GetQueuedCompletionStatus| + CloseHandle(mIocp); + mIocp = nullptr; + + mThread->Shutdown(); + } + + // close I/O Completion Port + if (mIocp && mIocp != INVALID_HANDLE_VALUE) { + CloseHandle(mIocp); + mIocp = nullptr; + } +} + +void NamedPipeService::RemoveRetiredObjects() { + MOZ_ASSERT(NS_GetCurrentThread() == mThread); + mLock.AssertCurrentThreadOwns(); + + if (!mRetiredHandles.IsEmpty()) { + for (auto& handle : mRetiredHandles) { + CloseHandle(handle); + } + mRetiredHandles.Clear(); + } + + mRetiredObservers.Clear(); +} + +/** + * Implement nsINamedPipeService + */ + +NS_IMETHODIMP +NamedPipeService::AddDataObserver(void* aHandle, + nsINamedPipeDataObserver* aObserver) { + if (!aHandle || aHandle == INVALID_HANDLE_VALUE || !aObserver) { + return NS_ERROR_ILLEGAL_VALUE; + } + + nsresult rv; + + HANDLE h = CreateIoCompletionPort(aHandle, mIocp, + reinterpret_cast<ULONG_PTR>(aObserver), 1); + if (NS_WARN_IF(!h)) { + LOG_NPS_ERROR("CreateIoCompletionPort error (%d)", GetLastError()); + return NS_ERROR_FAILURE; + } + if (NS_WARN_IF(h != mIocp)) { + LOG_NPS_ERROR( + "CreateIoCompletionPort got unexpected value %p (should be %p)", h, + mIocp); + CloseHandle(h); + return NS_ERROR_FAILURE; + } + + { + MutexAutoLock lock(mLock); + MOZ_ASSERT(!mObservers.Contains(aObserver)); + + mObservers.AppendElement(aObserver); + + // start event loop + if (mObservers.Length() == 1) { + rv = mThread->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG_NPS_ERROR("Dispatch to thread failed (%08x)", rv); + mObservers.Clear(); + return rv; + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +NamedPipeService::RemoveDataObserver(void* aHandle, + nsINamedPipeDataObserver* aObserver) { + MutexAutoLock lock(mLock); + mObservers.RemoveElement(aObserver); + + mRetiredHandles.AppendElement(aHandle); + mRetiredObservers.AppendElement(aObserver); + + return NS_OK; +} + +NS_IMETHODIMP +NamedPipeService::IsOnCurrentThread(bool* aRetVal) { + MOZ_ASSERT(mThread); + MOZ_ASSERT(aRetVal); + + if (!mThread) { + *aRetVal = false; + return NS_OK; + } + + return mThread->IsOnCurrentThread(aRetVal); +} + +/** + * Implement nsIObserver + */ + +NS_IMETHODIMP +NamedPipeService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!strcmp(NS_XPCOM_SHUTDOWN_OBSERVER_ID, aTopic)) { + Shutdown(); + } + + return NS_OK; +} + +/** + * Implement nsIRunnable + */ + +NS_IMETHODIMP +NamedPipeService::Run() { + MOZ_ASSERT(NS_GetCurrentThread() == mThread); + MOZ_ASSERT(mIocp && mIocp != INVALID_HANDLE_VALUE); + + while (!mIsShutdown) { + { + MutexAutoLock lock(mLock); + if (mObservers.IsEmpty()) { + LOG_NPS_DEBUG("no observer, stop loop"); + break; + } + + RemoveRetiredObjects(); + } + + DWORD bytesTransferred = 0; + ULONG_PTR key = 0; + LPOVERLAPPED overlapped = nullptr; + BOOL success = + GetQueuedCompletionStatus(mIocp, &bytesTransferred, &key, &overlapped, + 1000); // timeout, 1s + auto err = GetLastError(); + if (!success) { + if (err == WAIT_TIMEOUT) { + continue; + } else if (err == ERROR_ABANDONED_WAIT_0) { // mIocp was closed + break; + } else if (!overlapped) { + /** + * Did not dequeue a completion packet from the completion port, and + * bytesTransferred/key are meaningless. + * See remarks of |GetQueuedCompletionStatus| API. + */ + + LOG_NPS_ERROR("invalid overlapped (%d)", err); + continue; + } + + MOZ_ASSERT(key); + } + + /** + * Windows doesn't provide a method to remove created I/O Completion Port, + * all we can do is just close the handle we monitored before. + * In some cases, there's race condition that the monitored handle has an + * I/O status after the observer is being removed and destroyed. + * To avoid changing the ref-count of a dangling pointer, don't use nsCOMPtr + * here. + */ + nsINamedPipeDataObserver* target = + reinterpret_cast<nsINamedPipeDataObserver*>(key); + + nsCOMPtr<nsINamedPipeDataObserver> obs; + { + MutexAutoLock lock(mLock); + + auto idx = mObservers.IndexOf(target); + if (idx == decltype(mObservers)::NoIndex) { + LOG_NPS_ERROR("observer %p not found", target); + continue; + } + obs = target; + } + + MOZ_ASSERT(obs.get()); + + if (success) { + LOG_NPS_DEBUG("OnDataAvailable: obs=%p, bytes=%d", obs.get(), + bytesTransferred); + obs->OnDataAvailable(bytesTransferred, overlapped); + } else { + LOG_NPS_ERROR("GetQueuedCompletionStatus %p failed, error=%d", obs.get(), + err); + obs->OnError(err, overlapped); + } + } + + { + MutexAutoLock lock(mLock); + RemoveRetiredObjects(); + } + + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/socket/nsNamedPipeService.h b/netwerk/socket/nsNamedPipeService.h new file mode 100644 index 0000000000..5d6e5f86fe --- /dev/null +++ b/netwerk/socket/nsNamedPipeService.h @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_netwerk_socket_nsNamedPipeService_h +#define mozilla_netwerk_socket_nsNamedPipeService_h + +#include <windows.h> +#include "mozilla/Atomics.h" +#include "mozilla/Mutex.h" +#include "nsINamedPipeService.h" +#include "nsIObserver.h" +#include "nsIRunnable.h" +#include "nsIThread.h" +#include "nsTArray.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla { +namespace net { + +class NamedPipeService final : public nsINamedPipeService, + public nsIObserver, + public nsIRunnable { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSINAMEDPIPESERVICE + NS_DECL_NSIOBSERVER + NS_DECL_NSIRUNNABLE + + static already_AddRefed<nsINamedPipeService> GetOrCreate(); + + private: + explicit NamedPipeService(); + virtual ~NamedPipeService() = default; + + nsresult Init(); + + void Shutdown(); + void RemoveRetiredObjects(); + + HANDLE mIocp; // native handle to the I/O completion port. + Atomic<bool> + mIsShutdown; // set to true to stop the event loop running by mThread. + nsCOMPtr<nsIThread> mThread; // worker thread to get I/O events. + + /** + * The observers is maintained in |mObservers| to ensure valid life-cycle. + * We don't remove the handle and corresponding observer directly, instead + * the handle and observer into a "retired" list and close/remove them in + * the worker thread to avoid a race condition that might happen between + * |CloseHandle()| and |GetQueuedCompletionStatus()|. + */ + Mutex mLock; + nsTArray<nsCOMPtr<nsINamedPipeDataObserver>> + mObservers; // protected by mLock + nsTArray<nsCOMPtr<nsINamedPipeDataObserver>> + mRetiredObservers; // protected by mLock + nsTArray<HANDLE> mRetiredHandles; // protected by mLock + + static StaticRefPtr<NamedPipeService> gSingleton; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_netwerk_socket_nsNamedPipeService_h diff --git a/netwerk/socket/nsSOCKSIOLayer.cpp b/netwerk/socket/nsSOCKSIOLayer.cpp new file mode 100644 index 0000000000..0a16d6c723 --- /dev/null +++ b/netwerk/socket/nsSOCKSIOLayer.cpp @@ -0,0 +1,1527 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set expandtab ts=4 sw=2 sts=2 cin: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nspr.h" +#include "private/pprio.h" +#include "nsString.h" +#include "nsCRT.h" + +#include "nsIDNSService.h" +#include "nsIDNSRecord.h" +#include "nsISOCKSSocketInfo.h" +#include "nsISocketProvider.h" +#include "nsNamedPipeIOLayer.h" +#include "nsSOCKSIOLayer.h" +#include "nsNetCID.h" +#include "nsIDNSListener.h" +#include "nsICancelable.h" +#include "nsThreadUtils.h" +#include "nsIFile.h" +#include "nsIFileProtocolHandler.h" +#include "mozilla/Logging.h" +#include "mozilla/net/DNS.h" +#include "mozilla/Unused.h" + +using mozilla::LogLevel; +using namespace mozilla::net; + +static PRDescIdentity nsSOCKSIOLayerIdentity; +static PRIOMethods nsSOCKSIOLayerMethods; +static bool firstTime = true; +static bool ipv6Supported = true; + +static mozilla::LazyLogModule gSOCKSLog("SOCKS"); +#define LOGDEBUG(args) MOZ_LOG(gSOCKSLog, mozilla::LogLevel::Debug, args) +#define LOGERROR(args) MOZ_LOG(gSOCKSLog, mozilla::LogLevel::Error, args) + +class nsSOCKSSocketInfo : public nsISOCKSSocketInfo, public nsIDNSListener { + enum State { + SOCKS_INITIAL, + SOCKS_DNS_IN_PROGRESS, + SOCKS_DNS_COMPLETE, + SOCKS_CONNECTING_TO_PROXY, + SOCKS4_WRITE_CONNECT_REQUEST, + SOCKS4_READ_CONNECT_RESPONSE, + SOCKS5_WRITE_AUTH_REQUEST, + SOCKS5_READ_AUTH_RESPONSE, + SOCKS5_WRITE_USERNAME_REQUEST, + SOCKS5_READ_USERNAME_RESPONSE, + SOCKS5_WRITE_CONNECT_REQUEST, + SOCKS5_READ_CONNECT_RESPONSE_TOP, + SOCKS5_READ_CONNECT_RESPONSE_BOTTOM, + SOCKS_CONNECTED, + SOCKS_FAILED + }; + + // A buffer of 520 bytes should be enough for any request and response + // in case of SOCKS4 as well as SOCKS5 + static const uint32_t BUFFER_SIZE = 520; + static const uint32_t MAX_HOSTNAME_LEN = 255; + static const uint32_t MAX_USERNAME_LEN = 255; + static const uint32_t MAX_PASSWORD_LEN = 255; + + public: + nsSOCKSSocketInfo(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISOCKSSOCKETINFO + NS_DECL_NSIDNSLISTENER + + void Init(int32_t version, int32_t family, nsIProxyInfo* proxy, + const char* destinationHost, uint32_t flags, uint32_t tlsFlags); + + void SetConnectTimeout(PRIntervalTime to); + PRStatus DoHandshake(PRFileDesc* fd, int16_t oflags = -1); + int16_t GetPollFlags() const; + bool IsConnected() const { return mState == SOCKS_CONNECTED; } + void ForgetFD() { mFD = nullptr; } + void SetNamedPipeFD(PRFileDesc* fd) { mFD = fd; } + + private: + virtual ~nsSOCKSSocketInfo() { + ForgetFD(); + HandshakeFinished(); + } + + void HandshakeFinished(PRErrorCode err = 0); + PRStatus StartDNS(PRFileDesc* fd); + PRStatus ConnectToProxy(PRFileDesc* fd); + void FixupAddressFamily(PRFileDesc* fd, NetAddr* proxy); + PRStatus ContinueConnectingToProxy(PRFileDesc* fd, int16_t oflags); + PRStatus WriteV4ConnectRequest(); + PRStatus ReadV4ConnectResponse(); + PRStatus WriteV5AuthRequest(); + PRStatus ReadV5AuthResponse(); + PRStatus WriteV5UsernameRequest(); + PRStatus ReadV5UsernameResponse(); + PRStatus WriteV5ConnectRequest(); + PRStatus ReadV5AddrTypeAndLength(uint8_t* type, uint32_t* len); + PRStatus ReadV5ConnectResponseTop(); + PRStatus ReadV5ConnectResponseBottom(); + + uint8_t ReadUint8(); + uint16_t ReadUint16(); + uint32_t ReadUint32(); + void ReadNetAddr(NetAddr* addr, uint16_t fam); + void ReadNetPort(NetAddr* addr); + + void WantRead(uint32_t sz); + PRStatus ReadFromSocket(PRFileDesc* fd); + PRStatus WriteToSocket(PRFileDesc* fd); + + bool IsLocalProxy() { + nsAutoCString proxyHost; + mProxy->GetHost(proxyHost); + return IsHostLocalTarget(proxyHost); + } + + nsresult SetLocalProxyPath(const nsACString& aLocalProxyPath, + NetAddr* aProxyAddr) { +#ifdef XP_UNIX + nsresult rv; + MOZ_ASSERT(aProxyAddr); + + nsCOMPtr<nsIProtocolHandler> protocolHandler( + do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "file", &rv)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFileProtocolHandler> fileHandler( + do_QueryInterface(protocolHandler, &rv)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFile> socketFile; + rv = fileHandler->GetFileFromURLSpec(aLocalProxyPath, + getter_AddRefs(socketFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString path; + if (NS_WARN_IF(NS_FAILED(rv = socketFile->GetNativePath(path)))) { + return rv; + } + + if (sizeof(aProxyAddr->local.path) <= path.Length()) { + NS_WARNING("domain socket path too long."); + return NS_ERROR_FAILURE; + } + + aProxyAddr->raw.family = AF_UNIX; + strcpy(aProxyAddr->local.path, path.get()); + + return NS_OK; +#elif defined(XP_WIN) + MOZ_ASSERT(aProxyAddr); + + if (sizeof(aProxyAddr->local.path) <= aLocalProxyPath.Length()) { + NS_WARNING("pipe path too long."); + return NS_ERROR_FAILURE; + } + + aProxyAddr->raw.family = AF_LOCAL; + strcpy(aProxyAddr->local.path, PromiseFlatCString(aLocalProxyPath).get()); + return NS_OK; +#else + mozilla::Unused << aLocalProxyPath; + mozilla::Unused << aProxyAddr; + return NS_ERROR_NOT_IMPLEMENTED; +#endif + } + + bool SetupNamedPipeLayer(PRFileDesc* fd) { +#if defined(XP_WIN) + if (IsLocalProxy()) { + // nsSOCKSIOLayer handshaking only works under blocking mode + // unfortunately. Remember named pipe's FD to switch between modes. + SetNamedPipeFD(fd->lower); + return true; + } +#endif + return false; + } + + private: + State mState; + uint8_t* mData; + uint8_t* mDataIoPtr; + uint32_t mDataLength; + uint32_t mReadOffset; + uint32_t mAmountToRead; + nsCOMPtr<nsIDNSRecord> mDnsRec; + nsCOMPtr<nsICancelable> mLookup; + nsresult mLookupStatus; + PRFileDesc* mFD; + + nsCString mDestinationHost; + nsCOMPtr<nsIProxyInfo> mProxy; + int32_t mVersion; // SOCKS version 4 or 5 + int32_t mDestinationFamily; + uint32_t mFlags; + uint32_t mTlsFlags; + NetAddr mInternalProxyAddr; + NetAddr mExternalProxyAddr; + NetAddr mDestinationAddr; + PRIntervalTime mTimeout; + nsCString mProxyUsername; // Cache, from mProxy +}; + +nsSOCKSSocketInfo::nsSOCKSSocketInfo() + : mState(SOCKS_INITIAL), + mDataIoPtr(nullptr), + mDataLength(0), + mReadOffset(0), + mAmountToRead(0), + mLookupStatus(NS_ERROR_NOT_INITIALIZED), + mFD(nullptr), + mVersion(-1), + mDestinationFamily(AF_INET), + mFlags(0), + mTlsFlags(0), + mTimeout(PR_INTERVAL_NO_TIMEOUT) { + this->mInternalProxyAddr.inet.family = 0; + this->mInternalProxyAddr.inet6.family = 0; + this->mInternalProxyAddr.inet6.port = 0; + this->mInternalProxyAddr.inet6.flowinfo = 0; + this->mInternalProxyAddr.inet6.scope_id = 0; + this->mInternalProxyAddr.local.family = 0; + this->mExternalProxyAddr.inet.family = 0; + this->mExternalProxyAddr.inet6.family = 0; + this->mExternalProxyAddr.inet6.port = 0; + this->mExternalProxyAddr.inet6.flowinfo = 0; + this->mExternalProxyAddr.inet6.scope_id = 0; + this->mExternalProxyAddr.local.family = 0; + this->mDestinationAddr.inet.family = 0; + this->mDestinationAddr.inet6.family = 0; + this->mDestinationAddr.inet6.port = 0; + this->mDestinationAddr.inet6.flowinfo = 0; + this->mDestinationAddr.inet6.scope_id = 0; + this->mDestinationAddr.local.family = 0; + mData = new uint8_t[BUFFER_SIZE]; + + mInternalProxyAddr.raw.family = AF_INET; + mInternalProxyAddr.inet.ip = htonl(INADDR_ANY); + mInternalProxyAddr.inet.port = htons(0); + + mExternalProxyAddr.raw.family = AF_INET; + mExternalProxyAddr.inet.ip = htonl(INADDR_ANY); + mExternalProxyAddr.inet.port = htons(0); + + mDestinationAddr.raw.family = AF_INET; + mDestinationAddr.inet.ip = htonl(INADDR_ANY); + mDestinationAddr.inet.port = htons(0); +} + +/* Helper template class to statically check that writes to a fixed-size + * buffer are not going to overflow. + * + * Example usage: + * uint8_t real_buf[TOTAL_SIZE]; + * Buffer<TOTAL_SIZE> buf(&real_buf); + * auto buf2 = buf.WriteUint16(1); + * auto buf3 = buf2.WriteUint8(2); + * + * It is possible to chain them, to limit the number of (error-prone) + * intermediate variables: + * auto buf = Buffer<TOTAL_SIZE>(&real_buf) + * .WriteUint16(1) + * .WriteUint8(2); + * + * Debug builds assert when intermediate variables are reused: + * Buffer<TOTAL_SIZE> buf(&real_buf); + * auto buf2 = buf.WriteUint16(1); + * auto buf3 = buf.WriteUint8(2); // Asserts + * + * Strings can be written, given an explicit maximum length. + * buf.WriteString<MAX_STRING_LENGTH>(str); + * + * The Written() method returns how many bytes have been written so far: + * Buffer<TOTAL_SIZE> buf(&real_buf); + * auto buf2 = buf.WriteUint16(1); + * auto buf3 = buf2.WriteUint8(2); + * buf3.Written(); // returns 3. + */ +template <size_t Size> +class Buffer { + public: + Buffer() : mBuf(nullptr), mLength(0) {} + + explicit Buffer(uint8_t* aBuf, size_t aLength = 0) + : mBuf(aBuf), mLength(aLength) {} + + template <size_t Size2> + MOZ_IMPLICIT Buffer(const Buffer<Size2>& aBuf) + : mBuf(aBuf.mBuf), mLength(aBuf.mLength) { + static_assert(Size2 > Size, "Cannot cast buffer"); + } + + Buffer<Size - sizeof(uint8_t)> WriteUint8(uint8_t aValue) { + return Write(aValue); + } + + Buffer<Size - sizeof(uint16_t)> WriteUint16(uint16_t aValue) { + return Write(aValue); + } + + Buffer<Size - sizeof(uint32_t)> WriteUint32(uint32_t aValue) { + return Write(aValue); + } + + Buffer<Size - sizeof(uint16_t)> WriteNetPort(const NetAddr* aAddr) { + return WriteUint16(aAddr->inet.port); + } + + Buffer<Size - sizeof(IPv6Addr)> WriteNetAddr(const NetAddr* aAddr) { + if (aAddr->raw.family == AF_INET) { + return Write(aAddr->inet.ip); + } else if (aAddr->raw.family == AF_INET6) { + return Write(aAddr->inet6.ip.u8); + } + MOZ_ASSERT_UNREACHABLE("Unknown address family"); + return *this; + } + + template <size_t MaxLength> + Buffer<Size - MaxLength> WriteString(const nsACString& aStr) { + if (aStr.Length() > MaxLength) { + return Buffer<Size - MaxLength>(nullptr); + } + return WritePtr<char, MaxLength>(aStr.Data(), aStr.Length()); + } + + size_t Written() { + MOZ_ASSERT(mBuf); + return mLength; + } + + explicit operator bool() { return !!mBuf; } + + private: + template <size_t Size2> + friend class Buffer; + + template <typename T> + Buffer<Size - sizeof(T)> Write(T& aValue) { + return WritePtr<T, sizeof(T)>(&aValue, sizeof(T)); + } + + template <typename T, size_t Length> + Buffer<Size - Length> WritePtr(const T* aValue, size_t aCopyLength) { + static_assert(Size >= Length, "Cannot write that much"); + MOZ_ASSERT(aCopyLength <= Length); + MOZ_ASSERT(mBuf); + memcpy(mBuf, aValue, aCopyLength); + Buffer<Size - Length> result(mBuf + aCopyLength, mLength + aCopyLength); + mBuf = nullptr; + mLength = 0; + return result; + } + + uint8_t* mBuf; + size_t mLength; +}; + +void nsSOCKSSocketInfo::Init(int32_t version, int32_t family, + nsIProxyInfo* proxy, const char* host, + uint32_t flags, uint32_t tlsFlags) { + mVersion = version; + mDestinationFamily = family; + mProxy = proxy; + mDestinationHost = host; + mFlags = flags; + mTlsFlags = tlsFlags; + mProxy->GetUsername(mProxyUsername); // cache +} + +NS_IMPL_ISUPPORTS(nsSOCKSSocketInfo, nsISOCKSSocketInfo, nsIDNSListener) + +NS_IMETHODIMP +nsSOCKSSocketInfo::GetExternalProxyAddr(NetAddr** aExternalProxyAddr) { + memcpy(*aExternalProxyAddr, &mExternalProxyAddr, sizeof(NetAddr)); + return NS_OK; +} + +NS_IMETHODIMP +nsSOCKSSocketInfo::SetExternalProxyAddr(NetAddr* aExternalProxyAddr) { + memcpy(&mExternalProxyAddr, aExternalProxyAddr, sizeof(NetAddr)); + return NS_OK; +} + +NS_IMETHODIMP +nsSOCKSSocketInfo::GetDestinationAddr(NetAddr** aDestinationAddr) { + memcpy(*aDestinationAddr, &mDestinationAddr, sizeof(NetAddr)); + return NS_OK; +} + +NS_IMETHODIMP +nsSOCKSSocketInfo::SetDestinationAddr(NetAddr* aDestinationAddr) { + memcpy(&mDestinationAddr, aDestinationAddr, sizeof(NetAddr)); + return NS_OK; +} + +NS_IMETHODIMP +nsSOCKSSocketInfo::GetInternalProxyAddr(NetAddr** aInternalProxyAddr) { + memcpy(*aInternalProxyAddr, &mInternalProxyAddr, sizeof(NetAddr)); + return NS_OK; +} + +NS_IMETHODIMP +nsSOCKSSocketInfo::SetInternalProxyAddr(NetAddr* aInternalProxyAddr) { + memcpy(&mInternalProxyAddr, aInternalProxyAddr, sizeof(NetAddr)); + return NS_OK; +} + +// There needs to be a means of distinguishing between connection errors +// that the SOCKS server reports when it rejects a connection request, and +// connection errors that happen while attempting to connect to the SOCKS +// server. Otherwise, Firefox will report incorrectly that the proxy server +// is refusing connections when a SOCKS request is rejected by the proxy. +// When a SOCKS handshake failure occurs, the PR error is set to +// PR_UNKNOWN_ERROR, and the real error code is returned via the OS error. +void nsSOCKSSocketInfo::HandshakeFinished(PRErrorCode err) { + if (err == 0) { + mState = SOCKS_CONNECTED; +#if defined(XP_WIN) + // Switch back to nonblocking mode after finishing handshaking. + if (IsLocalProxy() && mFD) { + PRSocketOptionData opt_nonblock; + opt_nonblock.option = PR_SockOpt_Nonblocking; + opt_nonblock.value.non_blocking = PR_TRUE; + PR_SetSocketOption(mFD, &opt_nonblock); + mFD = nullptr; + } +#endif + } else { + mState = SOCKS_FAILED; + PR_SetError(PR_UNKNOWN_ERROR, err); + } + + // We don't need the buffer any longer, so free it. + delete[] mData; + mData = nullptr; + mDataIoPtr = nullptr; + mDataLength = 0; + mReadOffset = 0; + mAmountToRead = 0; + if (mLookup) { + mLookup->Cancel(NS_ERROR_FAILURE); + mLookup = nullptr; + } +} + +PRStatus nsSOCKSSocketInfo::StartDNS(PRFileDesc* fd) { + MOZ_ASSERT(!mDnsRec && mState == SOCKS_INITIAL, + "Must be in initial state to make DNS Lookup"); + + nsCOMPtr<nsIDNSService> dns = do_GetService(NS_DNSSERVICE_CONTRACTID); + if (!dns) return PR_FAILURE; + + nsCString proxyHost; + mProxy->GetHost(proxyHost); + + mozilla::OriginAttributes attrs; + + mFD = fd; + nsresult rv = dns->AsyncResolveNative( + proxyHost, nsIDNSService::RESOLVE_TYPE_DEFAULT, + nsIDNSService::RESOLVE_IGNORE_SOCKS_DNS, nullptr, this, + mozilla::GetCurrentEventTarget(), attrs, getter_AddRefs(mLookup)); + + if (NS_FAILED(rv)) { + LOGERROR(("socks: DNS lookup for SOCKS proxy %s failed", proxyHost.get())); + return PR_FAILURE; + } + mState = SOCKS_DNS_IN_PROGRESS; + PR_SetError(PR_IN_PROGRESS_ERROR, 0); + return PR_FAILURE; +} + +NS_IMETHODIMP +nsSOCKSSocketInfo::OnLookupComplete(nsICancelable* aRequest, + nsIDNSRecord* aRecord, nsresult aStatus) { + MOZ_ASSERT(aRequest == mLookup, "wrong DNS query"); + mLookup = nullptr; + mLookupStatus = aStatus; + mDnsRec = aRecord; + mState = SOCKS_DNS_COMPLETE; + if (mFD) { + ConnectToProxy(mFD); + ForgetFD(); + } + return NS_OK; +} + +PRStatus nsSOCKSSocketInfo::ConnectToProxy(PRFileDesc* fd) { + PRStatus status; + nsresult rv; + + MOZ_ASSERT(mState == SOCKS_DNS_COMPLETE, "Must have DNS to make connection!"); + + if (NS_FAILED(mLookupStatus)) { + PR_SetError(PR_BAD_ADDRESS_ERROR, 0); + return PR_FAILURE; + } + + // Try socks5 if the destination addrress is IPv6 + if (mVersion == 4 && mDestinationAddr.raw.family == AF_INET6) { + mVersion = 5; + } + + nsAutoCString proxyHost; + mProxy->GetHost(proxyHost); + + int32_t proxyPort; + mProxy->GetPort(&proxyPort); + + int32_t addresses = 0; + do { + if (IsLocalProxy()) { + rv = SetLocalProxyPath(proxyHost, &mInternalProxyAddr); + if (NS_FAILED(rv)) { + LOGERROR( + ("socks: unable to connect to SOCKS proxy, %s", proxyHost.get())); + return PR_FAILURE; + } + } else { + nsCOMPtr<nsIDNSAddrRecord> record = do_QueryInterface(mDnsRec); + MOZ_ASSERT(record); + if (addresses++) { + record->ReportUnusable(proxyPort); + } + + rv = record->GetNextAddr(proxyPort, &mInternalProxyAddr); + // No more addresses to try? If so, we'll need to bail + if (NS_FAILED(rv)) { + LOGERROR( + ("socks: unable to connect to SOCKS proxy, %s", proxyHost.get())); + return PR_FAILURE; + } + + if (MOZ_LOG_TEST(gSOCKSLog, LogLevel::Debug)) { + char buf[kIPv6CStrBufSize]; + mInternalProxyAddr.ToStringBuffer(buf, sizeof(buf)); + LOGDEBUG(("socks: trying proxy server, %s:%hu", buf, + ntohs(mInternalProxyAddr.inet.port))); + } + } + + NetAddr proxy = mInternalProxyAddr; + FixupAddressFamily(fd, &proxy); + PRNetAddr prProxy; + NetAddrToPRNetAddr(&proxy, &prProxy); + status = fd->lower->methods->connect(fd->lower, &prProxy, mTimeout); + if (status != PR_SUCCESS) { + PRErrorCode c = PR_GetError(); + + // If EINPROGRESS, return now and check back later after polling + if (c == PR_WOULD_BLOCK_ERROR || c == PR_IN_PROGRESS_ERROR) { + mState = SOCKS_CONNECTING_TO_PROXY; + return status; + } else if (IsLocalProxy()) { + LOGERROR(("socks: connect to domain socket failed (%d)", c)); + PR_SetError(PR_CONNECT_REFUSED_ERROR, 0); + mState = SOCKS_FAILED; + return status; + } + } + } while (status != PR_SUCCESS); + +#if defined(XP_WIN) + // Switch to blocking mode during handshaking + if (IsLocalProxy() && mFD) { + PRSocketOptionData opt_nonblock; + opt_nonblock.option = PR_SockOpt_Nonblocking; + opt_nonblock.value.non_blocking = PR_FALSE; + PR_SetSocketOption(mFD, &opt_nonblock); + } +#endif + + // Connected now, start SOCKS + if (mVersion == 4) return WriteV4ConnectRequest(); + return WriteV5AuthRequest(); +} + +void nsSOCKSSocketInfo::FixupAddressFamily(PRFileDesc* fd, NetAddr* proxy) { + int32_t proxyFamily = mInternalProxyAddr.raw.family; + // Do nothing if the address family is already matched + if (proxyFamily == mDestinationFamily) { + return; + } + // If the system does not support IPv6 and the proxy address is IPv6, + // We can do nothing here. + if (proxyFamily == AF_INET6 && !ipv6Supported) { + return; + } + // If the system does not support IPv6 and the destination address is + // IPv6, convert IPv4 address to IPv4-mapped IPv6 address to satisfy + // the emulation layer + if (mDestinationFamily == AF_INET6 && !ipv6Supported) { + proxy->inet6.family = AF_INET6; + proxy->inet6.port = mInternalProxyAddr.inet.port; + uint8_t* proxyp = proxy->inet6.ip.u8; + memset(proxyp, 0, 10); + memset(proxyp + 10, 0xff, 2); + memcpy(proxyp + 12, (char*)&mInternalProxyAddr.inet.ip, 4); + // mDestinationFamily should not be updated + return; + } + // There's no PR_NSPR_IO_LAYER required when using named pipe, + // we simply ignore the TCP family here. + if (SetupNamedPipeLayer(fd)) { + return; + } + + // Get an OS native handle from a specified FileDesc + PROsfd osfd = PR_FileDesc2NativeHandle(fd); + if (osfd == -1) { + return; + } + + // Create a new FileDesc with a specified family + PRFileDesc* tmpfd = PR_OpenTCPSocket(proxyFamily); + if (!tmpfd) { + return; + } + PROsfd newsd = PR_FileDesc2NativeHandle(tmpfd); + if (newsd == -1) { + PR_Close(tmpfd); + return; + } + // Must succeed because PR_FileDesc2NativeHandle succeeded + fd = PR_GetIdentitiesLayer(fd, PR_NSPR_IO_LAYER); + MOZ_ASSERT(fd); + // Swap OS native handles + PR_ChangeFileDescNativeHandle(fd, newsd); + PR_ChangeFileDescNativeHandle(tmpfd, osfd); + // Close temporary FileDesc which is now associated with + // old OS native handle + PR_Close(tmpfd); + mDestinationFamily = proxyFamily; +} + +PRStatus nsSOCKSSocketInfo::ContinueConnectingToProxy(PRFileDesc* fd, + int16_t oflags) { + PRStatus status; + + MOZ_ASSERT(mState == SOCKS_CONNECTING_TO_PROXY, + "Continuing connection in wrong state!"); + + LOGDEBUG(("socks: continuing connection to proxy")); + + status = fd->lower->methods->connectcontinue(fd->lower, oflags); + if (status != PR_SUCCESS) { + PRErrorCode c = PR_GetError(); + if (c != PR_WOULD_BLOCK_ERROR && c != PR_IN_PROGRESS_ERROR) { + // A connection failure occured, try another address + mState = SOCKS_DNS_COMPLETE; + return ConnectToProxy(fd); + } + + // We're still connecting + return PR_FAILURE; + } + + // Connected now, start SOCKS + if (mVersion == 4) return WriteV4ConnectRequest(); + return WriteV5AuthRequest(); +} + +PRStatus nsSOCKSSocketInfo::WriteV4ConnectRequest() { + if (mProxyUsername.Length() > MAX_USERNAME_LEN) { + LOGERROR(("socks username is too long")); + HandshakeFinished(PR_UNKNOWN_ERROR); + return PR_FAILURE; + } + + NetAddr* addr = &mDestinationAddr; + int32_t proxy_resolve; + + MOZ_ASSERT(mState == SOCKS_CONNECTING_TO_PROXY, "Invalid state!"); + + proxy_resolve = mFlags & nsISocketProvider::PROXY_RESOLVES_HOST; + + mDataLength = 0; + mState = SOCKS4_WRITE_CONNECT_REQUEST; + + LOGDEBUG(("socks4: sending connection request (socks4a resolve? %s)", + proxy_resolve ? "yes" : "no")); + + // Send a SOCKS 4 connect request. + auto buf = Buffer<BUFFER_SIZE>(mData) + .WriteUint8(0x04) // version -- 4 + .WriteUint8(0x01) // command -- connect + .WriteNetPort(addr); + + // We don't have anything more to write after the if, so we can + // use a buffer with no further writes allowed. + Buffer<0> buf3; + if (proxy_resolve) { + // Add the full name, null-terminated, to the request + // according to SOCKS 4a. A fake IP address, with the first + // four bytes set to 0 and the last byte set to something other + // than 0, is used to notify the proxy that this is a SOCKS 4a + // request. This request type works for Tor and perhaps others. + // Passwords not supported by V4. + auto buf2 = + buf.WriteUint32(htonl(0x00000001)) // Fake IP + .WriteString<MAX_USERNAME_LEN>(mProxyUsername) + .WriteUint8(0x00) // Null-terminate username + .WriteString<MAX_HOSTNAME_LEN>(mDestinationHost); // Hostname + if (!buf2) { + LOGERROR(("socks4: destination host name is too long!")); + HandshakeFinished(PR_BAD_ADDRESS_ERROR); + return PR_FAILURE; + } + buf3 = buf2.WriteUint8(0x00); + } else if (addr->raw.family == AF_INET) { + // Passwords not supported by V4. + buf3 = buf.WriteNetAddr(addr) // Add the IPv4 address + .WriteString<MAX_USERNAME_LEN>(mProxyUsername) + .WriteUint8(0x00); // Null-terminate username + } else { + LOGERROR(("socks: SOCKS 4 can only handle IPv4 addresses!")); + HandshakeFinished(PR_BAD_ADDRESS_ERROR); + return PR_FAILURE; + } + + mDataLength = buf3.Written(); + return PR_SUCCESS; +} + +PRStatus nsSOCKSSocketInfo::ReadV4ConnectResponse() { + MOZ_ASSERT(mState == SOCKS4_READ_CONNECT_RESPONSE, + "Handling SOCKS 4 connection reply in wrong state!"); + MOZ_ASSERT(mDataLength == 8, "SOCKS 4 connection reply must be 8 bytes!"); + + LOGDEBUG(("socks4: checking connection reply")); + + if (ReadUint8() != 0x00) { + LOGERROR(("socks4: wrong connection reply")); + HandshakeFinished(PR_CONNECT_REFUSED_ERROR); + return PR_FAILURE; + } + + // See if our connection request was granted + if (ReadUint8() == 90) { + LOGDEBUG(("socks4: connection successful!")); + HandshakeFinished(); + return PR_SUCCESS; + } + + LOGERROR(("socks4: unable to connect")); + HandshakeFinished(PR_CONNECT_REFUSED_ERROR); + return PR_FAILURE; +} + +PRStatus nsSOCKSSocketInfo::WriteV5AuthRequest() { + MOZ_ASSERT(mVersion == 5, "SOCKS version must be 5!"); + + mDataLength = 0; + mState = SOCKS5_WRITE_AUTH_REQUEST; + + // Send an initial SOCKS 5 greeting + LOGDEBUG(("socks5: sending auth methods")); + mDataLength = Buffer<BUFFER_SIZE>(mData) + .WriteUint8(0x05) // version -- 5 + .WriteUint8(0x01) // # of auth methods -- 1 + // Use authenticate iff we have a proxy username. + .WriteUint8(mProxyUsername.IsEmpty() ? 0x00 : 0x02) + .Written(); + + return PR_SUCCESS; +} + +PRStatus nsSOCKSSocketInfo::ReadV5AuthResponse() { + MOZ_ASSERT(mState == SOCKS5_READ_AUTH_RESPONSE, + "Handling SOCKS 5 auth method reply in wrong state!"); + MOZ_ASSERT(mDataLength == 2, "SOCKS 5 auth method reply must be 2 bytes!"); + + LOGDEBUG(("socks5: checking auth method reply")); + + // Check version number + if (ReadUint8() != 0x05) { + LOGERROR(("socks5: unexpected version in the reply")); + HandshakeFinished(PR_CONNECT_REFUSED_ERROR); + return PR_FAILURE; + } + + // Make sure our authentication choice was accepted, + // and continue accordingly + uint8_t authMethod = ReadUint8(); + if (mProxyUsername.IsEmpty() && authMethod == 0x00) { // no auth + LOGDEBUG(("socks5: server allows connection without authentication")); + return WriteV5ConnectRequest(); + } else if (!mProxyUsername.IsEmpty() && authMethod == 0x02) { // username/pw + LOGDEBUG(("socks5: auth method accepted by server")); + return WriteV5UsernameRequest(); + } else { // 0xFF signals error + LOGERROR(("socks5: server did not accept our authentication method")); + HandshakeFinished(PR_CONNECT_REFUSED_ERROR); + return PR_FAILURE; + } +} + +PRStatus nsSOCKSSocketInfo::WriteV5UsernameRequest() { + MOZ_ASSERT(mVersion == 5, "SOCKS version must be 5!"); + + if (mProxyUsername.Length() > MAX_USERNAME_LEN) { + LOGERROR(("socks username is too long")); + HandshakeFinished(PR_UNKNOWN_ERROR); + return PR_FAILURE; + } + + nsCString password; + mProxy->GetPassword(password); + if (password.Length() > MAX_PASSWORD_LEN) { + LOGERROR(("socks password is too long")); + HandshakeFinished(PR_UNKNOWN_ERROR); + return PR_FAILURE; + } + + mDataLength = 0; + mState = SOCKS5_WRITE_USERNAME_REQUEST; + + // RFC 1929 Username/password auth for SOCKS 5 + LOGDEBUG(("socks5: sending username and password")); + mDataLength = Buffer<BUFFER_SIZE>(mData) + .WriteUint8(0x01) // version 1 (not 5) + .WriteUint8(mProxyUsername.Length()) // username length + .WriteString<MAX_USERNAME_LEN>(mProxyUsername) // username + .WriteUint8(password.Length()) // password length + .WriteString<MAX_PASSWORD_LEN>( + password) // password. WARNING: Sent unencrypted! + .Written(); + + return PR_SUCCESS; +} + +PRStatus nsSOCKSSocketInfo::ReadV5UsernameResponse() { + MOZ_ASSERT(mState == SOCKS5_READ_USERNAME_RESPONSE, + "Handling SOCKS 5 username/password reply in wrong state!"); + + MOZ_ASSERT(mDataLength == 2, "SOCKS 5 username reply must be 2 bytes"); + + // Check version number, must be 1 (not 5) + if (ReadUint8() != 0x01) { + LOGERROR(("socks5: unexpected version in the reply")); + HandshakeFinished(PR_CONNECT_REFUSED_ERROR); + return PR_FAILURE; + } + + // Check whether username/password were accepted + if (ReadUint8() != 0x00) { // 0 = success + LOGERROR(("socks5: username/password not accepted")); + HandshakeFinished(PR_CONNECT_REFUSED_ERROR); + return PR_FAILURE; + } + + LOGDEBUG(("socks5: username/password accepted by server")); + + return WriteV5ConnectRequest(); +} + +PRStatus nsSOCKSSocketInfo::WriteV5ConnectRequest() { + // Send SOCKS 5 connect request + NetAddr* addr = &mDestinationAddr; + int32_t proxy_resolve; + proxy_resolve = mFlags & nsISocketProvider::PROXY_RESOLVES_HOST; + + LOGDEBUG(("socks5: sending connection request (socks5 resolve? %s)", + proxy_resolve ? "yes" : "no")); + + mDataLength = 0; + mState = SOCKS5_WRITE_CONNECT_REQUEST; + + auto buf = Buffer<BUFFER_SIZE>(mData) + .WriteUint8(0x05) // version -- 5 + .WriteUint8(0x01) // command -- connect + .WriteUint8(0x00); // reserved + + // We're writing a net port after the if, so we need a buffer allowing + // to write that much. + Buffer<sizeof(uint16_t)> buf2; + // Add the address to the SOCKS 5 request. SOCKS 5 supports several + // address types, so we pick the one that works best for us. + if (proxy_resolve) { + // Add the host name. Only a single byte is used to store the length, + // so we must prevent long names from being used. + buf2 = buf.WriteUint8(0x03) // addr type -- domainname + .WriteUint8(mDestinationHost.Length()) // name length + .WriteString<MAX_HOSTNAME_LEN>(mDestinationHost); // Hostname + if (!buf2) { + LOGERROR(("socks5: destination host name is too long!")); + HandshakeFinished(PR_BAD_ADDRESS_ERROR); + return PR_FAILURE; + } + } else if (addr->raw.family == AF_INET) { + buf2 = buf.WriteUint8(0x01) // addr type -- IPv4 + .WriteNetAddr(addr); + } else if (addr->raw.family == AF_INET6) { + buf2 = buf.WriteUint8(0x04) // addr type -- IPv6 + .WriteNetAddr(addr); + } else { + LOGERROR(("socks5: destination address of unknown type!")); + HandshakeFinished(PR_BAD_ADDRESS_ERROR); + return PR_FAILURE; + } + + auto buf3 = buf2.WriteNetPort(addr); // port + mDataLength = buf3.Written(); + + return PR_SUCCESS; +} + +PRStatus nsSOCKSSocketInfo::ReadV5AddrTypeAndLength(uint8_t* type, + uint32_t* len) { + MOZ_ASSERT(mState == SOCKS5_READ_CONNECT_RESPONSE_TOP || + mState == SOCKS5_READ_CONNECT_RESPONSE_BOTTOM, + "Invalid state!"); + MOZ_ASSERT(mDataLength >= 5, + "SOCKS 5 connection reply must be at least 5 bytes!"); + + // Seek to the address location + mReadOffset = 3; + + *type = ReadUint8(); + + switch (*type) { + case 0x01: // ipv4 + *len = 4 - 1; + break; + case 0x04: // ipv6 + *len = 16 - 1; + break; + case 0x03: // fqdn + *len = ReadUint8(); + break; + default: // wrong address type + LOGERROR(("socks5: wrong address type in connection reply!")); + return PR_FAILURE; + } + + return PR_SUCCESS; +} + +PRStatus nsSOCKSSocketInfo::ReadV5ConnectResponseTop() { + uint8_t res; + uint32_t len; + + MOZ_ASSERT(mState == SOCKS5_READ_CONNECT_RESPONSE_TOP, "Invalid state!"); + MOZ_ASSERT(mDataLength == 5, + "SOCKS 5 connection reply must be exactly 5 bytes!"); + + LOGDEBUG(("socks5: checking connection reply")); + + // Check version number + if (ReadUint8() != 0x05) { + LOGERROR(("socks5: unexpected version in the reply")); + HandshakeFinished(PR_CONNECT_REFUSED_ERROR); + return PR_FAILURE; + } + + // Check response + res = ReadUint8(); + if (res != 0x00) { + PRErrorCode c = PR_CONNECT_REFUSED_ERROR; + + switch (res) { + case 0x01: + LOGERROR( + ("socks5: connect failed: " + "01, General SOCKS server failure.")); + break; + case 0x02: + LOGERROR( + ("socks5: connect failed: " + "02, Connection not allowed by ruleset.")); + break; + case 0x03: + LOGERROR(("socks5: connect failed: 03, Network unreachable.")); + c = PR_NETWORK_UNREACHABLE_ERROR; + break; + case 0x04: + LOGERROR(("socks5: connect failed: 04, Host unreachable.")); + c = PR_BAD_ADDRESS_ERROR; + break; + case 0x05: + LOGERROR(("socks5: connect failed: 05, Connection refused.")); + break; + case 0x06: + LOGERROR(("socks5: connect failed: 06, TTL expired.")); + c = PR_CONNECT_TIMEOUT_ERROR; + break; + case 0x07: + LOGERROR( + ("socks5: connect failed: " + "07, Command not supported.")); + break; + case 0x08: + LOGERROR( + ("socks5: connect failed: " + "08, Address type not supported.")); + c = PR_BAD_ADDRESS_ERROR; + break; + default: + LOGERROR(("socks5: connect failed.")); + break; + } + + HandshakeFinished(c); + return PR_FAILURE; + } + + if (ReadV5AddrTypeAndLength(&res, &len) != PR_SUCCESS) { + HandshakeFinished(PR_BAD_ADDRESS_ERROR); + return PR_FAILURE; + } + + mState = SOCKS5_READ_CONNECT_RESPONSE_BOTTOM; + WantRead(len + 2); + + return PR_SUCCESS; +} + +PRStatus nsSOCKSSocketInfo::ReadV5ConnectResponseBottom() { + uint8_t type; + uint32_t len; + + MOZ_ASSERT(mState == SOCKS5_READ_CONNECT_RESPONSE_BOTTOM, "Invalid state!"); + + if (ReadV5AddrTypeAndLength(&type, &len) != PR_SUCCESS) { + HandshakeFinished(PR_BAD_ADDRESS_ERROR); + return PR_FAILURE; + } + + MOZ_ASSERT(mDataLength == 7 + len, + "SOCKS 5 unexpected length of connection reply!"); + + LOGDEBUG(("socks5: loading source addr and port")); + // Read what the proxy says is our source address + switch (type) { + case 0x01: // ipv4 + ReadNetAddr(&mExternalProxyAddr, AF_INET); + break; + case 0x04: // ipv6 + ReadNetAddr(&mExternalProxyAddr, AF_INET6); + break; + case 0x03: // fqdn (skip) + mReadOffset += len; + mExternalProxyAddr.raw.family = AF_INET; + break; + } + + ReadNetPort(&mExternalProxyAddr); + + LOGDEBUG(("socks5: connected!")); + HandshakeFinished(); + + return PR_SUCCESS; +} + +void nsSOCKSSocketInfo::SetConnectTimeout(PRIntervalTime to) { mTimeout = to; } + +PRStatus nsSOCKSSocketInfo::DoHandshake(PRFileDesc* fd, int16_t oflags) { + LOGDEBUG(("socks: DoHandshake(), state = %d", mState)); + + switch (mState) { + case SOCKS_INITIAL: + if (IsLocalProxy()) { + mState = SOCKS_DNS_COMPLETE; + mLookupStatus = NS_OK; + return ConnectToProxy(fd); + } + + return StartDNS(fd); + case SOCKS_DNS_IN_PROGRESS: + PR_SetError(PR_IN_PROGRESS_ERROR, 0); + return PR_FAILURE; + case SOCKS_DNS_COMPLETE: + return ConnectToProxy(fd); + case SOCKS_CONNECTING_TO_PROXY: + return ContinueConnectingToProxy(fd, oflags); + case SOCKS4_WRITE_CONNECT_REQUEST: + if (WriteToSocket(fd) != PR_SUCCESS) return PR_FAILURE; + WantRead(8); + mState = SOCKS4_READ_CONNECT_RESPONSE; + return PR_SUCCESS; + case SOCKS4_READ_CONNECT_RESPONSE: + if (ReadFromSocket(fd) != PR_SUCCESS) return PR_FAILURE; + return ReadV4ConnectResponse(); + + case SOCKS5_WRITE_AUTH_REQUEST: + if (WriteToSocket(fd) != PR_SUCCESS) return PR_FAILURE; + WantRead(2); + mState = SOCKS5_READ_AUTH_RESPONSE; + return PR_SUCCESS; + case SOCKS5_READ_AUTH_RESPONSE: + if (ReadFromSocket(fd) != PR_SUCCESS) return PR_FAILURE; + return ReadV5AuthResponse(); + case SOCKS5_WRITE_USERNAME_REQUEST: + if (WriteToSocket(fd) != PR_SUCCESS) return PR_FAILURE; + WantRead(2); + mState = SOCKS5_READ_USERNAME_RESPONSE; + return PR_SUCCESS; + case SOCKS5_READ_USERNAME_RESPONSE: + if (ReadFromSocket(fd) != PR_SUCCESS) return PR_FAILURE; + return ReadV5UsernameResponse(); + case SOCKS5_WRITE_CONNECT_REQUEST: + if (WriteToSocket(fd) != PR_SUCCESS) return PR_FAILURE; + + // The SOCKS 5 response to the connection request is variable + // length. First, we'll read enough to tell how long the response + // is, and will read the rest later. + WantRead(5); + mState = SOCKS5_READ_CONNECT_RESPONSE_TOP; + return PR_SUCCESS; + case SOCKS5_READ_CONNECT_RESPONSE_TOP: + if (ReadFromSocket(fd) != PR_SUCCESS) return PR_FAILURE; + return ReadV5ConnectResponseTop(); + case SOCKS5_READ_CONNECT_RESPONSE_BOTTOM: + if (ReadFromSocket(fd) != PR_SUCCESS) return PR_FAILURE; + return ReadV5ConnectResponseBottom(); + + case SOCKS_CONNECTED: + LOGERROR(("socks: already connected")); + HandshakeFinished(PR_IS_CONNECTED_ERROR); + return PR_FAILURE; + case SOCKS_FAILED: + LOGERROR(("socks: already failed")); + return PR_FAILURE; + } + + LOGERROR(("socks: executing handshake in invalid state, %d", mState)); + HandshakeFinished(PR_INVALID_STATE_ERROR); + + return PR_FAILURE; +} + +int16_t nsSOCKSSocketInfo::GetPollFlags() const { + switch (mState) { + case SOCKS_DNS_IN_PROGRESS: + case SOCKS_DNS_COMPLETE: + case SOCKS_CONNECTING_TO_PROXY: + return PR_POLL_EXCEPT | PR_POLL_WRITE; + case SOCKS4_WRITE_CONNECT_REQUEST: + case SOCKS5_WRITE_AUTH_REQUEST: + case SOCKS5_WRITE_USERNAME_REQUEST: + case SOCKS5_WRITE_CONNECT_REQUEST: + return PR_POLL_WRITE; + case SOCKS4_READ_CONNECT_RESPONSE: + case SOCKS5_READ_AUTH_RESPONSE: + case SOCKS5_READ_USERNAME_RESPONSE: + case SOCKS5_READ_CONNECT_RESPONSE_TOP: + case SOCKS5_READ_CONNECT_RESPONSE_BOTTOM: + return PR_POLL_READ; + default: + break; + } + + return 0; +} + +inline uint8_t nsSOCKSSocketInfo::ReadUint8() { + uint8_t rv; + MOZ_ASSERT(mReadOffset + sizeof(rv) <= mDataLength, + "Not enough space to pop a uint8_t!"); + rv = mData[mReadOffset]; + mReadOffset += sizeof(rv); + return rv; +} + +inline uint16_t nsSOCKSSocketInfo::ReadUint16() { + uint16_t rv; + MOZ_ASSERT(mReadOffset + sizeof(rv) <= mDataLength, + "Not enough space to pop a uint16_t!"); + memcpy(&rv, mData + mReadOffset, sizeof(rv)); + mReadOffset += sizeof(rv); + return rv; +} + +inline uint32_t nsSOCKSSocketInfo::ReadUint32() { + uint32_t rv; + MOZ_ASSERT(mReadOffset + sizeof(rv) <= mDataLength, + "Not enough space to pop a uint32_t!"); + memcpy(&rv, mData + mReadOffset, sizeof(rv)); + mReadOffset += sizeof(rv); + return rv; +} + +void nsSOCKSSocketInfo::ReadNetAddr(NetAddr* addr, uint16_t fam) { + uint32_t amt = 0; + const uint8_t* ip = mData + mReadOffset; + + addr->raw.family = fam; + if (fam == AF_INET) { + amt = sizeof(addr->inet.ip); + MOZ_ASSERT(mReadOffset + amt <= mDataLength, + "Not enough space to pop an ipv4 addr!"); + memcpy(&addr->inet.ip, ip, amt); + } else if (fam == AF_INET6) { + amt = sizeof(addr->inet6.ip.u8); + MOZ_ASSERT(mReadOffset + amt <= mDataLength, + "Not enough space to pop an ipv6 addr!"); + memcpy(addr->inet6.ip.u8, ip, amt); + } + + mReadOffset += amt; +} + +void nsSOCKSSocketInfo::ReadNetPort(NetAddr* addr) { + addr->inet.port = ReadUint16(); +} + +void nsSOCKSSocketInfo::WantRead(uint32_t sz) { + MOZ_ASSERT(mDataIoPtr == nullptr, + "WantRead() called while I/O already in progress!"); + MOZ_ASSERT(mDataLength + sz <= BUFFER_SIZE, "Can't read that much data!"); + mAmountToRead = sz; +} + +PRStatus nsSOCKSSocketInfo::ReadFromSocket(PRFileDesc* fd) { + int32_t rc; + const uint8_t* end; + + if (!mAmountToRead) { + LOGDEBUG(("socks: ReadFromSocket(), nothing to do")); + return PR_SUCCESS; + } + + if (!mDataIoPtr) { + mDataIoPtr = mData + mDataLength; + mDataLength += mAmountToRead; + } + + end = mData + mDataLength; + + while (mDataIoPtr < end) { + rc = PR_Read(fd, mDataIoPtr, end - mDataIoPtr); + if (rc <= 0) { + if (rc == 0) { + LOGERROR(("socks: proxy server closed connection")); + HandshakeFinished(PR_CONNECT_REFUSED_ERROR); + return PR_FAILURE; + } else if (PR_GetError() == PR_WOULD_BLOCK_ERROR) { + LOGDEBUG(("socks: ReadFromSocket(), want read")); + } + break; + } + + mDataIoPtr += rc; + } + + LOGDEBUG(("socks: ReadFromSocket(), have %u bytes total", + unsigned(mDataIoPtr - mData))); + if (mDataIoPtr == end) { + mDataIoPtr = nullptr; + mAmountToRead = 0; + mReadOffset = 0; + return PR_SUCCESS; + } + + return PR_FAILURE; +} + +PRStatus nsSOCKSSocketInfo::WriteToSocket(PRFileDesc* fd) { + int32_t rc; + const uint8_t* end; + + if (!mDataLength) { + LOGDEBUG(("socks: WriteToSocket(), nothing to do")); + return PR_SUCCESS; + } + + if (!mDataIoPtr) mDataIoPtr = mData; + + end = mData + mDataLength; + + while (mDataIoPtr < end) { + rc = PR_Write(fd, mDataIoPtr, end - mDataIoPtr); + if (rc < 0) { + if (PR_GetError() == PR_WOULD_BLOCK_ERROR) { + LOGDEBUG(("socks: WriteToSocket(), want write")); + } + break; + } + + mDataIoPtr += rc; + } + + if (mDataIoPtr == end) { + mDataIoPtr = nullptr; + mDataLength = 0; + mReadOffset = 0; + return PR_SUCCESS; + } + + return PR_FAILURE; +} + +static PRStatus nsSOCKSIOLayerConnect(PRFileDesc* fd, const PRNetAddr* addr, + PRIntervalTime to) { + PRStatus status; + NetAddr dst; + + nsSOCKSSocketInfo* info = (nsSOCKSSocketInfo*)fd->secret; + if (info == nullptr) return PR_FAILURE; + + if (addr->raw.family == PR_AF_INET6 && + PR_IsNetAddrType(addr, PR_IpAddrV4Mapped)) { + const uint8_t* srcp; + + LOGDEBUG(("socks: converting ipv4-mapped ipv6 address to ipv4")); + + // copied from _PR_ConvertToIpv4NetAddr() + dst.raw.family = AF_INET; + dst.inet.ip = htonl(INADDR_ANY); + dst.inet.port = htons(0); + srcp = addr->ipv6.ip.pr_s6_addr; + memcpy(&dst.inet.ip, srcp + 12, 4); + dst.inet.family = AF_INET; + dst.inet.port = addr->ipv6.port; + } else { + memcpy(&dst, addr, sizeof(dst)); + } + + info->SetDestinationAddr(&dst); + info->SetConnectTimeout(to); + + do { + status = info->DoHandshake(fd, -1); + } while (status == PR_SUCCESS && !info->IsConnected()); + + return status; +} + +static PRStatus nsSOCKSIOLayerConnectContinue(PRFileDesc* fd, int16_t oflags) { + PRStatus status; + + nsSOCKSSocketInfo* info = (nsSOCKSSocketInfo*)fd->secret; + if (info == nullptr) return PR_FAILURE; + + do { + status = info->DoHandshake(fd, oflags); + } while (status == PR_SUCCESS && !info->IsConnected()); + + return status; +} + +static int16_t nsSOCKSIOLayerPoll(PRFileDesc* fd, int16_t in_flags, + int16_t* out_flags) { + nsSOCKSSocketInfo* info = (nsSOCKSSocketInfo*)fd->secret; + if (info == nullptr) return PR_FAILURE; + + if (!info->IsConnected()) { + *out_flags = 0; + return info->GetPollFlags(); + } + + return fd->lower->methods->poll(fd->lower, in_flags, out_flags); +} + +static PRStatus nsSOCKSIOLayerClose(PRFileDesc* fd) { + nsSOCKSSocketInfo* info = (nsSOCKSSocketInfo*)fd->secret; + PRDescIdentity id = PR_GetLayersIdentity(fd); + + if (info && id == nsSOCKSIOLayerIdentity) { + info->ForgetFD(); + NS_RELEASE(info); + fd->identity = PR_INVALID_IO_LAYER; + } + + return fd->lower->methods->close(fd->lower); +} + +static PRFileDesc* nsSOCKSIOLayerAccept(PRFileDesc* fd, PRNetAddr* addr, + PRIntervalTime timeout) { + // TODO: implement SOCKS support for accept + return fd->lower->methods->accept(fd->lower, addr, timeout); +} + +static int32_t nsSOCKSIOLayerAcceptRead(PRFileDesc* sd, PRFileDesc** nd, + PRNetAddr** raddr, void* buf, + int32_t amount, + PRIntervalTime timeout) { + // TODO: implement SOCKS support for accept, then read from it + return sd->lower->methods->acceptread(sd->lower, nd, raddr, buf, amount, + timeout); +} + +static PRStatus nsSOCKSIOLayerBind(PRFileDesc* fd, const PRNetAddr* addr) { + // TODO: implement SOCKS support for bind (very similar to connect) + return fd->lower->methods->bind(fd->lower, addr); +} + +static PRStatus nsSOCKSIOLayerGetName(PRFileDesc* fd, PRNetAddr* addr) { + nsSOCKSSocketInfo* info = (nsSOCKSSocketInfo*)fd->secret; + + if (info != nullptr && addr != nullptr) { + NetAddr temp; + NetAddr* tempPtr = &temp; + if (info->GetExternalProxyAddr(&tempPtr) == NS_OK) { + NetAddrToPRNetAddr(tempPtr, addr); + return PR_SUCCESS; + } + } + + return PR_FAILURE; +} + +static PRStatus nsSOCKSIOLayerGetPeerName(PRFileDesc* fd, PRNetAddr* addr) { + nsSOCKSSocketInfo* info = (nsSOCKSSocketInfo*)fd->secret; + + if (info != nullptr && addr != nullptr) { + NetAddr temp; + NetAddr* tempPtr = &temp; + if (info->GetDestinationAddr(&tempPtr) == NS_OK) { + NetAddrToPRNetAddr(tempPtr, addr); + return PR_SUCCESS; + } + } + + return PR_FAILURE; +} + +static PRStatus nsSOCKSIOLayerListen(PRFileDesc* fd, int backlog) { + // TODO: implement SOCKS support for listen + return fd->lower->methods->listen(fd->lower, backlog); +} + +// add SOCKS IO layer to an existing socket +nsresult nsSOCKSIOLayerAddToSocket(int32_t family, const char* host, + int32_t port, nsIProxyInfo* proxy, + int32_t socksVersion, uint32_t flags, + uint32_t tlsFlags, PRFileDesc* fd, + nsISupports** info) { + NS_ENSURE_TRUE((socksVersion == 4) || (socksVersion == 5), + NS_ERROR_NOT_INITIALIZED); + + if (firstTime) { + // XXX hack until NSPR provides an official way to detect system IPv6 + // support (bug 388519) + PRFileDesc* tmpfd = PR_OpenTCPSocket(PR_AF_INET6); + if (!tmpfd) { + ipv6Supported = false; + } else { + // If the system does not support IPv6, NSPR will push + // IPv6-to-IPv4 emulation layer onto the native layer + ipv6Supported = PR_GetIdentitiesLayer(tmpfd, PR_NSPR_IO_LAYER) == tmpfd; + PR_Close(tmpfd); + } + + nsSOCKSIOLayerIdentity = PR_GetUniqueIdentity("SOCKS layer"); + nsSOCKSIOLayerMethods = *PR_GetDefaultIOMethods(); + + nsSOCKSIOLayerMethods.connect = nsSOCKSIOLayerConnect; + nsSOCKSIOLayerMethods.connectcontinue = nsSOCKSIOLayerConnectContinue; + nsSOCKSIOLayerMethods.poll = nsSOCKSIOLayerPoll; + nsSOCKSIOLayerMethods.bind = nsSOCKSIOLayerBind; + nsSOCKSIOLayerMethods.acceptread = nsSOCKSIOLayerAcceptRead; + nsSOCKSIOLayerMethods.getsockname = nsSOCKSIOLayerGetName; + nsSOCKSIOLayerMethods.getpeername = nsSOCKSIOLayerGetPeerName; + nsSOCKSIOLayerMethods.accept = nsSOCKSIOLayerAccept; + nsSOCKSIOLayerMethods.listen = nsSOCKSIOLayerListen; + nsSOCKSIOLayerMethods.close = nsSOCKSIOLayerClose; + + firstTime = false; + } + + LOGDEBUG(("Entering nsSOCKSIOLayerAddToSocket().")); + + PRFileDesc* layer; + PRStatus rv; + + layer = PR_CreateIOLayerStub(nsSOCKSIOLayerIdentity, &nsSOCKSIOLayerMethods); + if (!layer) { + LOGERROR(("PR_CreateIOLayerStub() failed.")); + return NS_ERROR_FAILURE; + } + + nsSOCKSSocketInfo* infoObject = new nsSOCKSSocketInfo(); + if (!infoObject) { + // clean up IOLayerStub + LOGERROR(("Failed to create nsSOCKSSocketInfo().")); + PR_Free(layer); // PR_CreateIOLayerStub() uses PR_Malloc(). + return NS_ERROR_FAILURE; + } + + NS_ADDREF(infoObject); + infoObject->Init(socksVersion, family, proxy, host, flags, tlsFlags); + layer->secret = (PRFilePrivate*)infoObject; + + PRDescIdentity fdIdentity = PR_GetLayersIdentity(fd); +#if defined(XP_WIN) + if (fdIdentity == mozilla::net::nsNamedPipeLayerIdentity) { + // remember named pipe fd on the info object so that we can switch + // blocking and non-blocking mode on the pipe later. + infoObject->SetNamedPipeFD(fd); + } +#endif + rv = PR_PushIOLayer(fd, fdIdentity, layer); + + if (rv == PR_FAILURE) { + LOGERROR(("PR_PushIOLayer() failed. rv = %x.", rv)); + NS_RELEASE(infoObject); + PR_Free(layer); // PR_CreateIOLayerStub() uses PR_Malloc(). + return NS_ERROR_FAILURE; + } + + *info = static_cast<nsISOCKSSocketInfo*>(infoObject); + NS_ADDREF(*info); + return NS_OK; +} + +bool IsHostLocalTarget(const nsACString& aHost) { +#if defined(XP_UNIX) + return StringBeginsWith(aHost, "file:"_ns); +#elif defined(XP_WIN) + return IsNamedPipePath(aHost); +#else + return false; +#endif // XP_UNIX +} diff --git a/netwerk/socket/nsSOCKSIOLayer.h b/netwerk/socket/nsSOCKSIOLayer.h new file mode 100644 index 0000000000..5b82f3827d --- /dev/null +++ b/netwerk/socket/nsSOCKSIOLayer.h @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef nsSOCKSIOLayer_h__ +#define nsSOCKSIOLayer_h__ + +#include "prio.h" +#include "nscore.h" +#include "nsIProxyInfo.h" + +nsresult nsSOCKSIOLayerAddToSocket(int32_t family, const char* host, + int32_t port, nsIProxyInfo* proxyInfo, + int32_t socksVersion, uint32_t flags, + uint32_t tlsFlags, PRFileDesc* fd, + nsISupports** info); + +bool IsHostLocalTarget(const nsACString& aHost); + +#endif /* nsSOCKSIOLayer_h__ */ diff --git a/netwerk/socket/nsSOCKSSocketProvider.cpp b/netwerk/socket/nsSOCKSSocketProvider.cpp new file mode 100644 index 0000000000..536e06c962 --- /dev/null +++ b/netwerk/socket/nsSOCKSSocketProvider.cpp @@ -0,0 +1,119 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsNamedPipeIOLayer.h" +#include "nsSOCKSSocketProvider.h" +#include "nsSOCKSIOLayer.h" +#include "nsCOMPtr.h" +#include "nsError.h" + +using mozilla::OriginAttributes; + +////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS(nsSOCKSSocketProvider, nsISocketProvider) + +nsresult nsSOCKSSocketProvider::CreateV4(nsISupports* aOuter, REFNSIID aIID, + void** aResult) { + nsresult rv; + nsCOMPtr<nsISocketProvider> inst = + new nsSOCKSSocketProvider(NS_SOCKS_VERSION_4); + if (!inst) + rv = NS_ERROR_OUT_OF_MEMORY; + else + rv = inst->QueryInterface(aIID, aResult); + return rv; +} + +nsresult nsSOCKSSocketProvider::CreateV5(nsISupports* aOuter, REFNSIID aIID, + void** aResult) { + nsresult rv; + nsCOMPtr<nsISocketProvider> inst = + new nsSOCKSSocketProvider(NS_SOCKS_VERSION_5); + if (!inst) + rv = NS_ERROR_OUT_OF_MEMORY; + else + rv = inst->QueryInterface(aIID, aResult); + return rv; +} + +// Per-platform implemenation of OpenTCPSocket helper function +// Different platforms have special cases to handle + +#if defined(XP_WIN) +// The proxy host on Windows may be a named pipe uri, in which +// case a named-pipe (rather than a socket) should be returned +static PRFileDesc* OpenTCPSocket(int32_t family, nsIProxyInfo* proxy) { + PRFileDesc* sock = nullptr; + + nsAutoCString proxyHost; + proxy->GetHost(proxyHost); + if (IsNamedPipePath(proxyHost)) { + sock = CreateNamedPipeLayer(); + } else { + sock = PR_OpenTCPSocket(family); + } + + return sock; +} +#elif defined(XP_UNIX) +// The proxy host on UNIX systems may point to a local file uri +// in which case we should create an AF_LOCAL (UNIX Domain) socket +// instead of the requested AF_INET or AF_INET6 socket. + +// Normally,this socket would get thrown out and recreated later on +// with the proper family, but we want to do it early here so that +// we can enforce seccomp policy to blacklist socket(AF_INET) calls +// to prevent the content sandbox from creating network requests +static PRFileDesc* OpenTCPSocket(int32_t family, nsIProxyInfo* proxy) { + nsAutoCString proxyHost; + proxy->GetHost(proxyHost); + if (StringBeginsWith(proxyHost, "file://"_ns)) { + family = AF_LOCAL; + } + + return PR_OpenTCPSocket(family); +} +#else +// Default, pass-through to PR_OpenTCPSocket +static PRFileDesc* OpenTCPSocket(int32_t family, nsIProxyInfo*) { + return PR_OpenTCPSocket(family); +} +#endif + +NS_IMETHODIMP +nsSOCKSSocketProvider::NewSocket(int32_t family, const char* host, int32_t port, + nsIProxyInfo* proxy, + const OriginAttributes& originAttributes, + uint32_t flags, uint32_t tlsFlags, + PRFileDesc** result, nsISupports** socksInfo) { + PRFileDesc* sock = OpenTCPSocket(family, proxy); + if (!sock) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsresult rv = nsSOCKSIOLayerAddToSocket(family, host, port, proxy, mVersion, + flags, tlsFlags, sock, socksInfo); + if (NS_SUCCEEDED(rv)) { + *result = sock; + return NS_OK; + } + + return NS_ERROR_SOCKET_CREATE_FAILED; +} + +NS_IMETHODIMP +nsSOCKSSocketProvider::AddToSocket(int32_t family, const char* host, + int32_t port, nsIProxyInfo* proxy, + const OriginAttributes& originAttributes, + uint32_t flags, uint32_t tlsFlags, + PRFileDesc* sock, nsISupports** socksInfo) { + nsresult rv = nsSOCKSIOLayerAddToSocket(family, host, port, proxy, mVersion, + flags, tlsFlags, sock, socksInfo); + + if (NS_FAILED(rv)) rv = NS_ERROR_SOCKET_CREATE_FAILED; + return rv; +} diff --git a/netwerk/socket/nsSOCKSSocketProvider.h b/netwerk/socket/nsSOCKSSocketProvider.h new file mode 100644 index 0000000000..7dc34dae60 --- /dev/null +++ b/netwerk/socket/nsSOCKSSocketProvider.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef nsSOCKSSocketProvider_h__ +#define nsSOCKSSocketProvider_h__ + +#include "nsISocketProvider.h" + +// values for ctor's |version| argument +enum { NS_SOCKS_VERSION_4 = 4, NS_SOCKS_VERSION_5 = 5 }; + +class nsSOCKSSocketProvider : public nsISocketProvider { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISOCKETPROVIDER + + explicit nsSOCKSSocketProvider(uint32_t version) : mVersion(version) {} + + static nsresult CreateV4(nsISupports*, REFNSIID aIID, void** aResult); + static nsresult CreateV5(nsISupports*, REFNSIID aIID, void** aResult); + + private: + virtual ~nsSOCKSSocketProvider() = default; + + uint32_t mVersion; // NS_SOCKS_VERSION_4 or 5 +}; + +#endif /* nsSOCKSSocketProvider_h__ */ diff --git a/netwerk/socket/nsSocketProviderService.cpp b/netwerk/socket/nsSocketProviderService.cpp new file mode 100644 index 0000000000..93a84991f3 --- /dev/null +++ b/netwerk/socket/nsSocketProviderService.cpp @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsString.h" +#include "nsISocketProvider.h" +#include "nsError.h" +#include "nsNSSComponent.h" +#include "nsSOCKSSocketProvider.h" +#include "nsSocketProviderService.h" +#include "nsSSLSocketProvider.h" +#include "nsTLSSocketProvider.h" +#include "nsUDPSocketProvider.h" +#include "mozilla/ClearOnShutdown.h" + +mozilla::StaticRefPtr<nsSocketProviderService> + nsSocketProviderService::gSingleton; + +//////////////////////////////////////////////////////////////////////////////// + +already_AddRefed<nsISocketProviderService> +nsSocketProviderService::GetOrCreate() { + RefPtr<nsSocketProviderService> inst; + if (gSingleton) { + inst = gSingleton; + } else { + inst = new nsSocketProviderService(); + gSingleton = inst; + if (NS_IsMainThread()) { + mozilla::ClearOnShutdown(&gSingleton); + } else { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "net::nsSocketProviderService::GetOrCreate", + []() -> void { mozilla::ClearOnShutdown(&gSingleton); })); + } + } + return inst.forget(); +} + +NS_IMPL_ISUPPORTS(nsSocketProviderService, nsISocketProviderService) + +//////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +nsSocketProviderService::GetSocketProvider(const char* type, + nsISocketProvider** result) { + nsCOMPtr<nsISocketProvider> inst; + if (!nsCRT::strcmp(type, "ssl") && + (XRE_IsParentProcess() || XRE_IsSocketProcess()) && + EnsureNSSInitializedChromeOrContent()) { + inst = new nsSSLSocketProvider(); + } else if (!nsCRT::strcmp(type, "starttls") && + (XRE_IsParentProcess() || XRE_IsSocketProcess()) && + EnsureNSSInitializedChromeOrContent()) { + inst = new nsTLSSocketProvider(); + } else if (!nsCRT::strcmp(type, "socks")) { + inst = new nsSOCKSSocketProvider(NS_SOCKS_VERSION_5); + } else if (!nsCRT::strcmp(type, "socks4")) { + inst = new nsSOCKSSocketProvider(NS_SOCKS_VERSION_4); + } else if (!nsCRT::strcmp(type, "udp")) { + inst = new nsUDPSocketProvider(); + } else { + return NS_ERROR_UNKNOWN_SOCKET_TYPE; + } + inst.forget(result); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/netwerk/socket/nsSocketProviderService.h b/netwerk/socket/nsSocketProviderService.h new file mode 100644 index 0000000000..85b2ba04a4 --- /dev/null +++ b/netwerk/socket/nsSocketProviderService.h @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef nsSocketProviderService_h__ +#define nsSocketProviderService_h__ + +#include "nsISocketProviderService.h" +#include "mozilla/StaticPtr.h" + +class nsSocketProviderService : public nsISocketProviderService { + nsSocketProviderService() = default; + virtual ~nsSocketProviderService() = default; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISOCKETPROVIDERSERVICE + + static already_AddRefed<nsISocketProviderService> GetOrCreate(); + + private: + static mozilla::StaticRefPtr<nsSocketProviderService> gSingleton; +}; + +#endif /* nsSocketProviderService_h__ */ diff --git a/netwerk/socket/nsUDPSocketProvider.cpp b/netwerk/socket/nsUDPSocketProvider.cpp new file mode 100644 index 0000000000..c81a1614d5 --- /dev/null +++ b/netwerk/socket/nsUDPSocketProvider.cpp @@ -0,0 +1,39 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsUDPSocketProvider.h" + +#include "nspr.h" + +using mozilla::OriginAttributes; + +NS_IMPL_ISUPPORTS(nsUDPSocketProvider, nsISocketProvider) + +NS_IMETHODIMP +nsUDPSocketProvider::NewSocket(int32_t aFamily, const char* aHost, + int32_t aPort, nsIProxyInfo* aProxy, + const OriginAttributes& originAttributes, + uint32_t aFlags, uint32_t aTlsFlags, + PRFileDesc** aFileDesc, + nsISupports** aSecurityInfo) { + NS_ENSURE_ARG_POINTER(aFileDesc); + + PRFileDesc* udpFD = PR_OpenUDPSocket(aFamily); + if (!udpFD) return NS_ERROR_FAILURE; + + *aFileDesc = udpFD; + return NS_OK; +} + +NS_IMETHODIMP +nsUDPSocketProvider::AddToSocket(int32_t aFamily, const char* aHost, + int32_t aPort, nsIProxyInfo* aProxy, + const OriginAttributes& originAttributes, + uint32_t aFlags, uint32_t aTlsFlags, + struct PRFileDesc* aFileDesc, + nsISupports** aSecurityInfo) { + // does not make sense to strap a UDP socket onto an existing socket + MOZ_ASSERT_UNREACHABLE("Cannot layer UDP socket on an existing socket"); + return NS_ERROR_UNEXPECTED; +} diff --git a/netwerk/socket/nsUDPSocketProvider.h b/netwerk/socket/nsUDPSocketProvider.h new file mode 100644 index 0000000000..fb1fe749d9 --- /dev/null +++ b/netwerk/socket/nsUDPSocketProvider.h @@ -0,0 +1,20 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef nsUDPSocketProvider_h__ +#define nsUDPSocketProvider_h__ + +#include "nsISocketProvider.h" +#include "mozilla/Attributes.h" + +class nsUDPSocketProvider final : public nsISocketProvider { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISOCKETPROVIDER + + private: + ~nsUDPSocketProvider() = default; +}; + +#endif /* nsUDPSocketProvider_h__ */ |