1#[cfg(any(test, rings_browser))]
12use std::collections::HashMap;
13use std::sync::Arc;
14use std::sync::Mutex;
15
16use bytes::Bytes;
17#[cfg(any(test, rings_browser))]
18use futures::channel::oneshot;
19use rings_core::dht::Did;
20use rings_core::session::SessionSk;
21use serde::Deserialize;
22use serde::Serialize;
23
24#[cfg(rings_browser)]
25use self::browser::execute_https_request;
26#[cfg(test)]
27use self::limits::checked_status_code;
28use self::limits::https_response_body_limit;
29use self::limits::usize_to_u64;
30#[cfg(rings_native)]
31use self::native::execute_https_request;
32#[cfg(all(test, rings_native))]
33use self::native::native_fetch_with_timeout;
34#[cfg(all(test, rings_native))]
35use self::native::select_native_https_egress;
36#[cfg(all(test, rings_native))]
37use self::native::NativeHttpsEgress;
38#[cfg(any(test, rings_browser))]
39use self::pending::PendingOnionHttpsRequest;
40use crate::error::Error;
41use crate::error::Result;
42use crate::extension::ext::Scope;
43use crate::onion::circuit::send_backward;
44#[cfg(any(test, rings_browser))]
45use crate::onion::circuit::OnionAuthenticatedPayload;
46use crate::onion::circuit::OnionBackwardPath;
47use crate::onion::circuit::OnionBackwardSequence;
48use crate::onion::circuit::OnionCircuitExitFrame;
49#[cfg(rings_browser)]
50use crate::onion::circuit::OnionCircuitHandler;
51use crate::onion::circuit::OnionCircuitId;
52use crate::onion::circuit::OnionCircuitPayload;
53use crate::onion::circuit::OnionForwardNonce;
54use crate::onion::circuit::OnionForwardSequence;
55use crate::onion::circuit::OnionLinkSender;
56#[cfg(any(test, rings_browser))]
57use crate::onion::circuit::OnionReturnId;
58use crate::onion::exit_accounting::OnionExitAccounting;
59use crate::onion::exit_accounting::OnionExitLease;
60use crate::onion::proxy::OnionProxyTarget;
61use crate::onion::proxy::ONION_PROXY_HTTPS_SERVICE;
62use crate::onion::replay::OnionForwardReplayKey;
63use crate::onion::replay::OnionForwardReplayPartitions;
64use crate::onion::replay::ReplayAdmission;
65#[cfg(any(test, rings_browser))]
66use crate::onion::OnionExitDescriptor;
67use crate::onion::OnionExitFailure;
68use crate::onion::OnionExitPolicy;
69use crate::onion::OnionExitTarget;
70use crate::onion::OnionRouteError;
71use crate::sync_lock::lock;
72
73const DEFAULT_HTTPS_RESPONSE_BODY_LIMIT_BYTES: u64 = 8 * 1024 * 1024;
74
75#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
77pub struct OnionHttpsRequest {
78 pub target: String,
80 pub method: String,
82 pub path: String,
84 pub headers: Vec<(String, String)>,
86 pub body: Vec<u8>,
88}
89
90#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
92pub struct OnionHttpsResponse {
93 pub status: u16,
95 pub headers: Vec<(String, String)>,
97 pub body: Vec<u8>,
99}
100
101#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
102pub(crate) enum OnionHttpsPayload {
103 Request(OnionHttpsRequest),
104 Response(OnionHttpsResponse),
105 Error(OnionExitFailure),
106}
107
108pub(crate) fn encode_https_payload(payload: OnionHttpsPayload) -> Result<OnionCircuitPayload> {
109 rings_codec::serialize(&payload)
110 .map(|body| {
111 OnionCircuitPayload::new(crate::onion::OnionServiceName::https(), Bytes::from(body))
112 })
113 .map_err(|_| Error::EncodeError)
114}
115
116fn decode_https_payload(payload: OnionCircuitPayload) -> Result<Option<OnionHttpsPayload>> {
117 if !payload.matches_service(ONION_PROXY_HTTPS_SERVICE) {
118 return Ok(None);
119 }
120 rings_codec::deserialize(payload.body.as_ref())
121 .map(Some)
122 .map_err(|_| Error::DecodeError)
123}
124
125#[cfg(any(test, rings_browser))]
127#[cfg_attr(test, derive(Default))]
128#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
129pub struct OnionHttpsClientRequest {
130 #[serde(default = "default_method")]
132 pub method: String,
133 #[serde(default)]
135 pub path: Option<String>,
136 #[serde(default)]
138 pub headers: Vec<(String, String)>,
139 #[serde(default)]
141 pub body: Vec<u8>,
142}
143
144#[cfg(any(test, rings_browser))]
146#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
147pub struct OnionHttpsClientResponse {
148 pub status: u16,
150 pub headers: Vec<(String, String)>,
152 pub body: Vec<u8>,
154}
155
156pub(crate) struct OnionHttpsRuntime {
158 #[cfg(any(test, rings_browser))]
159 pending: Mutex<HashMap<OnionCircuitId, PendingRequest>>,
160 exit_policy: Mutex<Option<OnionExitPolicy>>,
161 forward_replays: Mutex<OnionForwardReplayPartitions>,
162 accounting: OnionExitAccounting,
163 link_sender: OnionLinkSender,
164 #[cfg(rings_native)]
165 native_proxy: Mutex<Option<String>>,
166}
167
168impl Default for OnionHttpsRuntime {
169 fn default() -> Self {
170 Self::with_resources(OnionExitAccounting::default(), OnionLinkSender::default())
171 }
172}
173
174#[cfg(any(test, rings_browser))]
175struct PendingRequest {
176 expected_return_peer: Did,
177 expected_exit: OnionExitDescriptor,
178 return_id: OnionReturnId,
179 sender: oneshot::Sender<std::result::Result<OnionHttpsClientResponse, Error>>,
180}
181
182impl OnionHttpsRuntime {
183 #[cfg(any(test, rings_browser))]
185 pub(crate) fn new() -> Self {
186 Self::default()
187 }
188
189 pub(crate) fn with_resources(
191 accounting: OnionExitAccounting,
192 link_sender: OnionLinkSender,
193 ) -> Self {
194 Self {
195 #[cfg(any(test, rings_browser))]
196 pending: Mutex::new(HashMap::new()),
197 exit_policy: Mutex::new(None),
198 forward_replays: Mutex::new(OnionForwardReplayPartitions::default()),
199 accounting,
200 link_sender,
201 #[cfg(rings_native)]
202 native_proxy: Mutex::new(None),
203 }
204 }
205
206 #[cfg(rings_browser)]
207 pub(crate) fn link_sender(&self) -> OnionLinkSender {
208 self.link_sender.clone()
209 }
210
211 #[cfg(rings_native)]
212 pub(crate) fn set_native_proxy(&self, proxy: Option<String>) {
213 if let Ok(mut current) = self.native_proxy.lock() {
214 *current = proxy;
215 }
216 }
217
218 #[cfg(rings_native)]
219 pub(crate) fn native_proxy(&self) -> Option<String> {
220 self.native_proxy
221 .lock()
222 .ok()
223 .and_then(|proxy| proxy.clone())
224 }
225
226 #[cfg(all(test, rings_native))]
227 pub(crate) fn accounting_for_test(&self) -> OnionExitAccounting {
228 self.accounting.clone()
229 }
230
231 pub(crate) fn set_exit_policy(&self, policy: Option<OnionExitPolicy>) {
233 if let Ok(mut current) = self.exit_policy.lock() {
234 *current = policy;
235 }
236 }
237
238 #[cfg(any(test, rings_browser))]
240 pub(crate) fn begin_request(
241 self: &Arc<Self>,
242 expected_return_peer: Did,
243 expected_exit: OnionExitDescriptor,
244 return_id: OnionReturnId,
245 ) -> Result<(OnionCircuitId, PendingOnionHttpsRequest)> {
246 let mut pending = lock(&self.pending)?;
247 for _ in 0..16 {
248 let id = OnionCircuitId::random();
249 if pending.contains_key(&id) {
250 continue;
251 }
252 let (sender, receiver) = oneshot::channel();
253 pending.insert(id, PendingRequest {
254 expected_return_peer,
255 expected_exit,
256 return_id,
257 sender,
258 });
259 return Ok((
260 id,
261 PendingOnionHttpsRequest::new(self.clone(), id, receiver),
262 ));
263 }
264 Err(Error::OnionRouteError(
265 OnionRouteError::CircuitIdAllocationFailed,
266 ))
267 }
268
269 #[cfg(any(test, rings_browser))]
270 fn cancel_request(&self, id: OnionCircuitId) {
271 if let Ok(mut pending) = self.pending.lock() {
272 pending.remove(&id);
273 }
274 }
275
276 #[cfg(any(test, rings_browser))]
278 pub(crate) fn complete_payload(
279 &self,
280 from: Did,
281 id: OnionCircuitId,
282 payload: OnionAuthenticatedPayload,
283 ) {
284 let Some((pending, payload)) = self.take_pending_payload(from, id, payload) else {
285 return;
286 };
287 match decode_https_payload(payload) {
288 Ok(Some(OnionHttpsPayload::Response(response))) => {
289 let _ = pending.sender.send(Ok(OnionHttpsClientResponse {
290 status: response.status,
291 headers: response.headers,
292 body: response.body,
293 }));
294 }
295 Ok(Some(OnionHttpsPayload::Error(failure))) => {
296 let _ =
297 pending
298 .sender
299 .send(Err(Error::OnionRouteError(OnionRouteError::ExitFailure(
300 failure,
301 ))));
302 }
303 Ok(Some(OnionHttpsPayload::Request(_)) | None) => {
304 let _ = pending.sender.send(Err(Error::OnionRouteError(
305 OnionRouteError::UnexpectedBackwardPayload,
306 )));
307 }
308 Err(error) => {
309 let _ = pending.sender.send(Err(error));
310 }
311 }
312 }
313
314 #[cfg(any(test, rings_browser))]
315 fn take_pending_payload(
316 &self,
317 from: Did,
318 id: OnionCircuitId,
319 payload: OnionAuthenticatedPayload,
320 ) -> Option<(PendingRequest, OnionCircuitPayload)> {
321 let mut pending = self.pending.lock().ok()?;
322 let request = pending.remove(&id)?;
323 if request.expected_return_peer != from {
324 pending.insert(id, request);
325 return None;
326 }
327 match payload.into_verified_payload(request.return_id, &request.expected_exit) {
328 Ok(verified) => Some((request, verified.payload)),
329 Err(error) => {
330 let _ = request.sender.send(Err(error));
331 None
332 }
333 }
334 }
335
336 pub(crate) fn exit_policy(&self) -> Option<OnionExitPolicy> {
337 self.exit_policy
338 .lock()
339 .ok()
340 .and_then(|policy| policy.clone())
341 }
342
343 fn admit_exit_request(
344 &self,
345 policy: &OnionExitPolicy,
346 circuit_id: OnionCircuitId,
347 return_peer: Did,
348 bytes: u64,
349 ) -> Result<OnionExitLease> {
350 self.accounting
351 .admit(policy, circuit_id, return_peer, bytes)
352 }
353
354 fn record_exit_bytes(&self, policy: &OnionExitPolicy, bytes: u64) -> Result<()> {
355 self.accounting.record_bytes(policy, bytes)
356 }
357
358 fn remaining_exit_bytes(&self, policy: &OnionExitPolicy) -> Result<Option<u64>> {
359 self.accounting.remaining_bytes(policy)
360 }
361
362 fn consume_forward_nonce(
363 &self,
364 from: Did,
365 circuit_id: OnionCircuitId,
366 nonce: OnionForwardNonce,
367 ) -> Result<()> {
368 let mut replays = lock(&self.forward_replays)?;
369 match replays.consume(
370 from,
371 OnionForwardReplayKey::new(circuit_id, nonce),
372 rings_core::utils::get_epoch_ms(),
373 ) {
374 ReplayAdmission::Consumed => Ok(()),
375 ReplayAdmission::Duplicate => {
376 Err(Error::OnionRouteError(OnionRouteError::ForwardReplay))
377 }
378 ReplayAdmission::Full => Err(Error::NoPermission),
379 }
380 }
381
382 #[cfg(test)]
383 pub(crate) fn pending_len(&self) -> usize {
384 self.pending
385 .lock()
386 .map(|pending| pending.len())
387 .unwrap_or(0)
388 }
389}
390
391#[cfg(any(test, rings_browser))]
393pub(crate) fn client_request_from_url(
394 url: &str,
395 request: OnionHttpsClientRequest,
396) -> Result<(OnionProxyTarget, OnionHttpsRequest)> {
397 let (target, path) = parse_https_url(url)?;
398 let request = client_request_with_default_path(&target, request, path.as_str())?;
399 Ok((target, request))
400}
401
402#[cfg(any(test, rings_browser))]
403fn client_request_with_default_path(
404 target: &OnionProxyTarget,
405 request: OnionHttpsClientRequest,
406 default_path: &str,
407) -> Result<OnionHttpsRequest> {
408 let path = request.path.as_deref().unwrap_or(default_path);
409 Ok(OnionHttpsRequest {
410 target: target.authority(),
411 method: normalize_method(&request.method),
412 path: normalize_path(path)?,
413 headers: request.headers,
414 body: request.body,
415 })
416}
417
418#[cfg(any(test, rings_browser))]
419fn parse_https_url(url: &str) -> Result<(OnionProxyTarget, String)> {
420 let url = url.trim();
421 let (scheme, rest) = url.split_once("://").ok_or_else(|| {
422 Error::HttpRequestError(
423 "browser HTTPS onion proxy request URL must be absolute".to_string(),
424 )
425 })?;
426 if !scheme.eq_ignore_ascii_case("https") {
427 return Err(Error::HttpRequestError(format!(
428 "browser HTTPS onion proxy only supports https URLs, got scheme {scheme:?}"
429 )));
430 }
431 let authority_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
432 let (authority, suffix) = rest.split_at(authority_end);
433 if authority.contains('@') {
434 return Err(Error::HttpRequestError(
435 "browser HTTPS onion proxy URLs must not contain userinfo".to_string(),
436 ));
437 }
438 let authority = https_authority_with_default_port(authority)?;
439 let target = OnionProxyTarget::parse_authority(authority.as_str())?;
440 Ok((target, url_path(suffix)))
441}
442
443#[cfg(any(test, rings_browser))]
444fn https_authority_with_default_port(authority: &str) -> Result<String> {
445 let authority = authority.trim();
446 if authority.is_empty() {
447 return Err(Error::HttpRequestError(
448 "browser HTTPS onion proxy URL host must not be empty".to_string(),
449 ));
450 }
451
452 if let Some(rest) = authority.strip_prefix('[') {
453 let Some((host, suffix)) = rest.split_once(']') else {
454 return Err(Error::HttpRequestError(format!(
455 "invalid IPv6 HTTPS onion proxy authority {authority:?}"
456 )));
457 };
458 if host.is_empty() {
459 return Err(Error::HttpRequestError(
460 "browser HTTPS onion proxy URL host must not be empty".to_string(),
461 ));
462 }
463 return if suffix.is_empty() {
464 Ok(format!("[{host}]:443"))
465 } else if let Some(port) = suffix.strip_prefix(':') {
466 if port.is_empty() {
467 Err(Error::HttpRequestError(format!(
468 "HTTPS onion proxy authority {authority:?} has an empty port"
469 )))
470 } else {
471 Ok(authority.to_string())
472 }
473 } else {
474 Err(Error::HttpRequestError(format!(
475 "invalid IPv6 HTTPS onion proxy authority {authority:?}"
476 )))
477 };
478 }
479
480 if authority.contains('[') || authority.contains(']') {
481 return Err(Error::HttpRequestError(format!(
482 "invalid HTTPS onion proxy authority {authority:?}"
483 )));
484 }
485 let colon_count = authority.chars().filter(|ch| *ch == ':').count();
486 if colon_count > 1 {
487 return Err(Error::HttpRequestError(
488 "IPv6 HTTPS onion proxy URLs must use bracketed hosts".to_string(),
489 ));
490 }
491 if colon_count == 1 {
492 let Some((host, port)) = authority.rsplit_once(':') else {
493 return Err(Error::HttpRequestError(format!(
494 "invalid HTTPS onion proxy authority {authority:?}"
495 )));
496 };
497 if host.is_empty() || port.is_empty() {
498 return Err(Error::HttpRequestError(format!(
499 "invalid HTTPS onion proxy authority {authority:?}"
500 )));
501 }
502 Ok(authority.to_string())
503 } else {
504 Ok(format!("{authority}:443"))
505 }
506}
507
508#[cfg(any(test, rings_browser))]
509fn url_path(suffix: &str) -> String {
510 let path = suffix
511 .split_once('#')
512 .map_or(suffix, |(before_fragment, _)| before_fragment);
513 if path.is_empty() {
514 default_path()
515 } else if path.starts_with('?') {
516 format!("/{path}")
517 } else {
518 path.to_string()
519 }
520}
521
522#[cfg(rings_browser)]
524pub(crate) struct BrowserOnionCircuitHandler {
525 https: Arc<OnionHttpsRuntime>,
526 session_sk: SessionSk,
527}
528
529#[cfg(rings_browser)]
530impl BrowserOnionCircuitHandler {
531 pub(crate) fn new(https: Arc<OnionHttpsRuntime>, session_sk: SessionSk) -> Self {
533 Self { https, session_sk }
534 }
535}
536
537#[cfg(rings_browser)]
538#[async_trait::async_trait(?Send)]
539impl OnionCircuitHandler for BrowserOnionCircuitHandler {
540 async fn handle_exit(&self, scope: &Scope, frame: OnionCircuitExitFrame) -> Result<()> {
541 let _ = try_handle_https_exit_payload(&self.https, &self.session_sk, scope, frame).await?;
542 Ok(())
543 }
544
545 async fn handle_client(
546 &self,
547 _scope: &Scope,
548 from: Did,
549 circuit_id: OnionCircuitId,
550 payload: OnionAuthenticatedPayload,
551 ) -> Result<()> {
552 self.https.complete_payload(from, circuit_id, payload);
553 Ok(())
554 }
555}
556
557pub(crate) async fn try_handle_https_exit_payload(
558 runtime: &Arc<OnionHttpsRuntime>,
559 session_sk: &SessionSk,
560 scope: &Scope,
561 frame: OnionCircuitExitFrame,
562) -> Result<bool> {
563 if !frame.payload.matches_service(ONION_PROXY_HTTPS_SERVICE) {
564 return Ok(false);
565 }
566 let Some(payload) = (match decode_https_payload(frame.payload) {
567 Ok(payload) => payload,
568 Err(Error::DecodeError) => return Ok(false),
569 Err(error) => return Err(error),
570 }) else {
571 return Ok(false);
572 };
573 let response = match payload {
574 OnionHttpsPayload::Request(request) => {
575 match execute_exit_fetch(
576 runtime,
577 &request,
578 frame.circuit_id,
579 frame.return_peer,
580 frame.forward_nonce,
581 frame.forward_sequence,
582 )
583 .await
584 {
585 Ok(response) => OnionHttpsPayload::Response(response),
586 Err(error) => OnionHttpsPayload::Error(OnionExitFailure::from_error(&error)),
587 }
588 }
589 OnionHttpsPayload::Response(_) | OnionHttpsPayload::Error(_) => return Ok(true),
590 };
591 send_backward(
592 &runtime.link_sender,
593 scope,
594 session_sk,
595 OnionBackwardPath::new(
596 frame.circuit_id,
597 frame.return_peer,
598 frame.return_session_public_key,
599 frame.client,
600 ),
601 OnionBackwardSequence::FIRST,
602 encode_https_payload(response)?,
603 )
604 .await?;
605 Ok(true)
606}
607
608pub(crate) async fn execute_exit_fetch(
609 runtime: &OnionHttpsRuntime,
610 request: &OnionHttpsRequest,
611 circuit_id: OnionCircuitId,
612 return_peer: Did,
613 forward_nonce: OnionForwardNonce,
614 forward_sequence: OnionForwardSequence,
615) -> Result<OnionHttpsResponse> {
616 if forward_sequence != OnionForwardSequence::FIRST {
617 return Err(Error::OnionRouteError(OnionRouteError::ForwardReplay));
618 }
619 runtime.consume_forward_nonce(return_peer, circuit_id, forward_nonce)?;
620 let target = OnionProxyTarget::parse_authority(&request.target)?;
621 let authority = target.authority();
622 let exit_target = OnionExitTarget::from_proxy_target(&target);
623 let Some(policy) = runtime.exit_policy() else {
624 return Err(Error::InvalidConfig(
625 "browser HTTPS onion exit is not enabled locally".to_string(),
626 ));
627 };
628 if !policy.allows_target(&exit_target) {
629 return Err(Error::NoPermission);
630 }
631 let request_body_bytes = usize_to_u64(request.body.len())?;
632 let _lease =
633 runtime.admit_exit_request(&policy, circuit_id, return_peer, request_body_bytes)?;
634 let body_limit = https_response_body_limit(runtime.remaining_exit_bytes(&policy)?);
635 if body_limit == 0 {
636 return Err(Error::NoPermission);
637 }
638 let url = format!("https://{}{}", authority, normalize_path(&request.path)?);
639 let response =
640 execute_https_request(&url, &target, request, body_limit, runtime, &policy).await?;
641 Ok(OnionHttpsResponse {
642 status: response.status,
643 headers: response.headers,
644 body: response.body,
645 })
646}
647
648pub(super) struct FetchResponse {
649 status: u16,
650 headers: Vec<(String, String)>,
651 body: Vec<u8>,
652}
653
654fn normalize_method(method: &str) -> String {
655 let method = method.trim();
656 if method.is_empty() {
657 default_method()
658 } else {
659 method.to_ascii_uppercase()
660 }
661}
662
663fn normalize_path(path: &str) -> Result<String> {
664 let path = path.trim();
665 if path.is_empty() {
666 return Ok(default_path());
667 }
668 if path.starts_with('/') {
669 return Ok(path.to_string());
670 }
671 if path.starts_with('?') {
672 return Ok(format!("/{path}"));
673 }
674 Err(Error::HttpRequestError(format!(
675 "browser HTTPS onion proxy path must start with '/' or '?', got {path:?}"
676 )))
677}
678
679fn default_method() -> String {
680 "GET".to_string()
681}
682
683fn default_path() -> String {
684 "/".to_string()
685}
686
687#[cfg(test)]
688mod tests;
689
690#[cfg(rings_browser)]
691mod browser;
692mod limits;
693#[cfg(rings_native)]
694mod native;
695#[cfg(any(test, rings_browser))]
696mod pending;