1#![expect(
6 clippy::allow_attributes,
7 reason = "feature-gated `mut self` consumed by some cfg branches but not others β `#[allow(unused_mut)]` would warn unfulfilled in the cfg arm where it IS used"
8)]
9
10use crate::{
11 Layer, Service,
12 cli::ForwardKind,
13 combinators::Either,
14 combinators::Either7,
15 error::{BoxError, BoxErrorExt, ErrorExt as _},
16 extensions::ExtensionsRef,
17 http::BodyLimitLayer,
18 http::{
19 Request, Response, StatusCode,
20 headers::exotic::XClacksOverhead,
21 headers::forwarded::{CFConnectingIp, ClientIp, TrueClientIp, XClientIp, XRealIp},
22 headers::{Accept, HeaderMapExt},
23 layer::{
24 forwarded::GetForwardedHeaderLayer, required_header::AddRequiredResponseHeadersLayer,
25 set_header::SetResponseHeaderLayer, trace::TraceLayer,
26 },
27 mime,
28 server::HttpServer,
29 service::web::response::{Css, IntoResponse, Json, Redirect, Script},
30 },
31 io::Io,
32 layer::limit::policy::UnlimitedPolicy,
33 layer::{ConsumeErrLayer, LimitLayer, TimeoutLayer, limit::policy::ConcurrentPolicy},
34 net::address::ip::geo::{GeoLocation, IpGeoDb, IpGeoInfo},
35 net::forwarded::Forwarded,
36 net::stream::SocketInfo,
37 proxy::haproxy::server::HaProxyLayer,
38 rt::Executor,
39 tcp::TcpStream,
40 telemetry::tracing,
41 utils::octets::mib,
42};
43
44#[cfg(all(feature = "rustls", not(feature = "boring")))]
45use crate::tls::rustls::server::{TlsAcceptorData, TlsAcceptorLayer};
46
47#[cfg(any(feature = "rustls", feature = "boring"))]
48use crate::http::headers::StrictTransportSecurity;
49
50#[cfg(feature = "boring")]
51use crate::{
52 net::tls::server::ServerConfig,
53 tls::boring::server::{TlsAcceptorData, TlsAcceptorLayer},
54};
55
56#[cfg(feature = "boring")]
57type TlsConfig = ServerConfig;
58
59#[cfg(all(feature = "rustls", not(feature = "boring")))]
60type TlsConfig = TlsAcceptorData;
61
62use std::{convert::Infallible, marker::PhantomData, net::IpAddr, sync::Arc, time::Duration};
63use tokio::io::AsyncWriteExt;
64
65#[derive(Debug, Clone)]
66pub struct IpServiceBuilder<M> {
69 #[cfg(any(feature = "rustls", feature = "boring"))]
70 tls_server_config: Option<TlsConfig>,
71 concurrent_limit: usize,
72 timeout: Duration,
73 forward: Option<ForwardKind>,
74 geo_db: Option<Arc<IpGeoDb>>,
75 _mode: PhantomData<fn(M)>,
76}
77
78impl IpServiceBuilder<mode::Http> {
79 #[must_use]
81 pub fn http() -> Self {
82 Self {
83 #[cfg(any(feature = "rustls", feature = "boring"))]
84 tls_server_config: None,
85 concurrent_limit: 0,
86 timeout: Duration::ZERO,
87 forward: None,
88 geo_db: None,
89 _mode: PhantomData,
90 }
91 }
92}
93
94impl IpServiceBuilder<mode::Transport> {
95 #[must_use]
97 pub fn tcp() -> Self {
98 Self {
99 #[cfg(any(feature = "rustls", feature = "boring"))]
100 tls_server_config: None,
101 concurrent_limit: 0,
102 timeout: Duration::ZERO,
103 forward: None,
104 geo_db: None,
105 _mode: PhantomData,
106 }
107 }
108}
109
110impl<M> IpServiceBuilder<M> {
111 crate::utils::macros::generate_set_and_with! {
112 #[must_use]
114 pub fn concurrent(mut self, limit: usize) -> Self {
115 self.concurrent_limit = limit;
116 self
117 }
118 }
119
120 crate::utils::macros::generate_set_and_with! {
121 #[must_use]
123 pub fn timeout(mut self, timeout: Duration) -> Self {
124 self.timeout = timeout;
125 self
126 }
127 }
128
129 crate::utils::macros::generate_set_and_with! {
130 #[must_use]
142 pub fn forward(mut self, maybe_kind: Option<ForwardKind>) -> Self {
143 self.forward = maybe_kind;
144 self
145 }
146 }
147
148 crate::utils::macros::generate_set_and_with! {
149 #[must_use]
152 pub fn geo_db(mut self, db: Option<Arc<IpGeoDb>>) -> Self {
153 self.geo_db = db;
154 self
155 }
156 }
157
158 crate::utils::macros::generate_set_and_with! {
159 #[cfg(any(feature = "rustls", feature = "boring"))]
160 pub fn tls_server_config(mut self, cfg: Option<TlsConfig>) -> Self {
163 self.tls_server_config = cfg;
164 self
165 }
166 }
167}
168
169impl IpServiceBuilder<mode::Http> {
170 #[allow(unused_mut)]
171 #[inline]
172 pub fn build(
174 mut self,
175 executor: Executor,
176 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
177 #[cfg(all(feature = "rustls", not(feature = "boring")))]
178 let tls_cfg = self.tls_server_config.take();
179
180 #[cfg(feature = "boring")]
181 let tls_cfg: Option<TlsAcceptorData> = match self.tls_server_config.take() {
182 Some(cfg) => Some(cfg.try_into()?),
183 None => None,
184 };
185
186 #[cfg(any(feature = "rustls", feature = "boring"))]
187 {
188 let maybe_tls_acceptor_layer = tls_cfg.map(TlsAcceptorLayer::new);
189 self.build_http(executor, maybe_tls_acceptor_layer)
190 }
191
192 #[cfg(not(any(feature = "rustls", feature = "boring")))]
193 self.build_http(executor)
194 }
195}
196
197#[derive(Debug, Clone)]
198struct HttpIpService {
203 geo_db: Option<Arc<IpGeoDb>>,
206}
207
208impl Service<Request> for HttpIpService {
209 type Output = Response;
210 type Error = Infallible;
211
212 async fn serve(&self, req: Request) -> Result<Self::Output, Self::Error> {
213 let peer_ip = req
214 .extensions()
215 .get_ref::<Forwarded>()
216 .and_then(|f| f.client_ip())
217 .or_else(|| {
218 req.extensions()
219 .get_ref::<SocketInfo>()
220 .map(|s| s.peer_addr().ip_addr)
221 });
222
223 Ok(match peer_ip {
224 Some(ip) => match HttpBodyContentFormat::derive_from_req(&req) {
225 HttpBodyContentFormat::Txt => ip.to_string().into_response(),
226 HttpBodyContentFormat::Html => {
227 let geo = self.geo_db.as_ref().and_then(|db| db.resolve(ip));
228 let attributions: Vec<_> = self
229 .geo_db
230 .as_ref()
231 .map(|db| db.attributions().collect())
232 .unwrap_or_default();
233 render_html_page(ip, geo.as_ref(), &attributions).into_response()
234 }
235 HttpBodyContentFormat::Json => {
236 let geo = self.geo_db.as_ref().and_then(|db| db.resolve(ip));
237 let mut body = serde_json::json!({ "ip": ip });
238 if let Some(info) = geo {
239 body["geo"] = serde_json::to_value(&info).unwrap_or_default();
241 }
242 Json(body).into_response()
243 }
244 },
245 None => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
246 })
247 }
248}
249
250const IP_STYLE_CSS: &str = include_str!("ip.css");
254
255const IP_SCRIPT_JS: &str = include_str!("ip.js");
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
260enum HttpBodyContentFormat {
261 #[default]
262 Txt,
263 Html,
264 Json,
265}
266
267impl HttpBodyContentFormat {
268 fn derive_from_req(req: &Request) -> Self {
269 let Some(accept) = req.headers().typed_get::<Accept>() else {
270 return Self::default();
271 };
272 let mut entries: Vec<_> = accept.0.iter().collect();
275 entries.sort_by_key(|qv| std::cmp::Reverse(qv.quality));
276 entries
277 .into_iter()
278 .find_map(|qv| {
279 let r#type = qv.value.subtype();
280 if r#type == mime::JSON {
281 Some(Self::Json)
282 } else if r#type == mime::HTML {
283 Some(Self::Html)
284 } else if r#type == mime::TEXT {
285 Some(Self::Txt)
286 } else {
287 None
288 }
289 })
290 .unwrap_or_default()
291 }
292}
293
294#[derive(Debug, Clone)]
295#[non_exhaustive]
296struct TcpIpService;
298
299impl<Input> Service<Input> for TcpIpService
300where
301 Input: Io + Unpin + ExtensionsRef,
302{
303 type Output = ();
304 type Error = BoxError;
305
306 async fn serve(&self, stream: Input) -> Result<Self::Output, Self::Error> {
307 tracing::info!("connection received");
308 let peer_ip = stream
309 .extensions()
310 .get_ref::<Forwarded>()
311 .and_then(|f| f.client_ip())
312 .or_else(|| {
313 stream
314 .extensions()
315 .get_ref::<SocketInfo>()
316 .map(|s| s.peer_addr().ip_addr)
317 });
318 let Some(peer_ip) = peer_ip else {
319 tracing::error!("missing peer information");
320 return Ok(());
321 };
322
323 let mut stream = std::pin::pin!(stream);
324
325 match peer_ip {
326 std::net::IpAddr::V4(ip) => {
327 if let Err(err) = stream.write_all(&ip.octets()).await {
328 tracing::error!("error writing IPv4 of peer to peer: {}", err);
329 }
330 }
331 std::net::IpAddr::V6(ip) => {
332 if let Err(err) = stream.write_all(&ip.octets()).await {
333 tracing::error!("error writing IPv6 of peer to peer: {}", err);
334 }
335 }
336 };
337
338 Ok(())
339 }
340}
341
342impl IpServiceBuilder<mode::Transport> {
343 #[allow(unused_mut)]
344 #[inline]
345 pub fn build(
347 mut self,
348 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
349 #[cfg(all(feature = "rustls", not(feature = "boring")))]
350 let tls_cfg = self.tls_server_config.take();
351
352 #[cfg(feature = "boring")]
353 let tls_cfg: Option<TlsAcceptorData> = match self.tls_server_config.take() {
354 Some(cfg) => Some(cfg.try_into()?),
355 None => None,
356 };
357
358 #[cfg(any(feature = "rustls", feature = "boring"))]
359 {
360 let maybe_tls_acceptor_layer = tls_cfg.map(TlsAcceptorLayer::new);
361 self.build_tcp(maybe_tls_acceptor_layer)
362 }
363
364 #[cfg(not(any(feature = "rustls", feature = "boring")))]
365 self.build_tcp()
366 }
367}
368
369impl<M> IpServiceBuilder<M> {
370 fn build_tcp<S: Io + ExtensionsRef + Unpin + Sync>(
371 self,
372 #[cfg(any(feature = "rustls", feature = "boring"))] maybe_tls_accept_layer: Option<
373 TlsAcceptorLayer,
374 >,
375 ) -> Result<impl Service<S, Output = (), Error = Infallible>, BoxError> {
376 let tcp_forwarded_layer = match &self.forward {
377 None => None,
378 Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
379 Some(other) => {
380 return Err(
381 BoxError::from_static_str("invalid forward kind for Transport mode")
382 .with_context_debug_field("kind", || other.clone()),
383 );
384 }
385 };
386
387 let tcp_service_builder = (
388 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
389 LimitLayer::new(if self.concurrent_limit > 0 {
390 Either::A(ConcurrentPolicy::max(self.concurrent_limit))
391 } else {
392 Either::B(UnlimitedPolicy::new())
393 }),
394 if !self.timeout.is_zero() {
395 TimeoutLayer::new(self.timeout)
396 } else {
397 TimeoutLayer::never()
398 },
399 tcp_forwarded_layer,
400 #[cfg(any(feature = "rustls", feature = "boring"))]
401 maybe_tls_accept_layer,
402 );
403
404 Ok(tcp_service_builder.into_layer(TcpIpService))
405 }
406
407 fn build_http<S: Io + Unpin + Sync + ExtensionsRef>(
408 self,
409 executor: Executor,
410 #[cfg(any(feature = "rustls", feature = "boring"))] maybe_tls_accept_layer: Option<
411 TlsAcceptorLayer,
412 >,
413 ) -> Result<impl Service<S, Output = (), Error = Infallible>, BoxError> {
414 let (tcp_forwarded_layer, http_forwarded_layer) = match &self.forward {
415 None => (None, None),
416 Some(ForwardKind::Forwarded) => {
417 (None, Some(Either7::A(GetForwardedHeaderLayer::forwarded())))
418 }
419 Some(ForwardKind::XForwardedFor) => (
420 None,
421 Some(Either7::B(GetForwardedHeaderLayer::x_forwarded_for())),
422 ),
423 Some(ForwardKind::XClientIp) => (
424 None,
425 Some(Either7::C(GetForwardedHeaderLayer::<XClientIp>::new())),
426 ),
427 Some(ForwardKind::ClientIp) => (
428 None,
429 Some(Either7::D(GetForwardedHeaderLayer::<ClientIp>::new())),
430 ),
431 Some(ForwardKind::XRealIp) => (
432 None,
433 Some(Either7::E(GetForwardedHeaderLayer::<XRealIp>::new())),
434 ),
435 Some(ForwardKind::CFConnectingIp) => (
436 None,
437 Some(Either7::F(GetForwardedHeaderLayer::<CFConnectingIp>::new())),
438 ),
439 Some(ForwardKind::TrueClientIp) => (
440 None,
441 Some(Either7::G(GetForwardedHeaderLayer::<TrueClientIp>::new())),
442 ),
443 Some(ForwardKind::HaProxy) => (Some(HaProxyLayer::default()), None),
444 };
445
446 #[cfg(any(feature = "rustls", feature = "boring"))]
447 let hsts_layer = maybe_tls_accept_layer.is_some().then(|| {
448 SetResponseHeaderLayer::if_not_present_typed(
449 StrictTransportSecurity::excluding_subdomains_for_max_seconds(31536000),
450 )
451 });
452
453 let tcp_service_builder = (
454 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
455 (self.concurrent_limit > 0)
456 .then(|| LimitLayer::new(ConcurrentPolicy::max(self.concurrent_limit))),
457 (!self.timeout.is_zero()).then(|| TimeoutLayer::new(self.timeout)),
458 tcp_forwarded_layer,
459 BodyLimitLayer::request_only(mib(1)),
461 #[cfg(any(feature = "rustls", feature = "boring"))]
462 maybe_tls_accept_layer,
463 );
464
465 let (csp_layer, nosniff_layer, referrer_layer, frame_layer) =
473 crate::cli::service::http_security::defence_in_depth_layer(
474 crate::cli::service::http_security::rama_html_csp(),
475 );
476
477 let geo_attribution = self.geo_db.as_ref().and_then(|db| {
479 let notices: Vec<_> = db.attributions().collect();
480 (!notices.is_empty()).then(|| crate::cli::service::geo::geo_attribution_layer(notices))
481 });
482
483 let router = crate::http::service::web::Router::new()
487 .with_get(
488 "/",
489 HttpIpService {
490 geo_db: self.geo_db,
491 },
492 )
493 .with_get("/style/ip.css", Css(IP_STYLE_CSS))
494 .with_get("/script/ip.js", Script(IP_SCRIPT_JS))
495 .with_not_found(async || Redirect::permanent("/"));
496
497 let http_service = (
498 TraceLayer::new_for_http(),
499 SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
500 AddRequiredResponseHeadersLayer::default(),
501 geo_attribution,
502 csp_layer,
503 nosniff_layer,
504 referrer_layer,
505 frame_layer,
506 ConsumeErrLayer::default(),
507 #[cfg(any(feature = "rustls", feature = "boring"))]
508 hsts_layer,
509 http_forwarded_layer,
510 )
511 .into_layer(router);
512
513 let http_service = Arc::new(http_service);
517 Ok(tcp_service_builder.into_layer(HttpServer::auto(executor).service(http_service)))
518 }
519}
520
521pub mod mode {
522 #[derive(Debug, Clone)]
525 #[non_exhaustive]
526 pub struct Http;
528
529 #[derive(Debug, Clone)]
530 #[non_exhaustive]
531 pub struct Transport;
533}
534
535fn render_html_page(
536 ip: IpAddr,
537 geo: Option<&IpGeoInfo>,
538 attributions: &[&str],
539) -> impl crate::http::protocols::html::IntoHtml + IntoResponse {
540 use crate::http::protocols::html::*;
541
542 let geo_comment =
544 crate::cli::service::geo::geo_attribution_html_comment(attributions).map(PreEscaped);
545 let geo_panel = geo.map(|info| {
546 let rows = |loc: &GeoLocation| {
547 crate::cli::service::geo::geo_location_rows(loc)
548 .into_iter()
549 .map(|(k, v)| div!(class = "georow", div!(class = "muted", k), div!(code!(v))))
550 .collect::<Vec<_>>()
551 };
552 let card = |label: String, loc: &GeoLocation| {
554 div!(
555 class = "panel geo-card",
556 div!(class = "muted geo-source", label),
557 rows(loc),
558 )
559 };
560 let mut cards = vec![card("merged".to_owned(), &info.location)];
561 cards.extend(
562 info.by_source
563 .iter()
564 .map(|src| card(src.label.to_string(), &src.location)),
565 );
566 div!(
567 class = "geo-section",
568 role = "region",
569 "aria-label" = "geo panel",
570 div!(class = "muted geo-title", "Geolocation"),
571 div!(class = "geo-grid", cards),
572 )
573 });
574
575 html!(
576 lang = "en",
577 head!(
578 meta!(charset = "utf-8"),
579 meta!(
580 name = "viewport",
581 content = "width=device-width,initial-scale=1"
582 ),
583 link!(
584 rel = "icon",
585 href = PreEscaped(
586 "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>\
587 <text y='0.9em' font-size='90'>π¦</text></svg>"
588 ),
589 ),
590 title!("Rama IP"),
591 link!(
592 rel = "stylesheet",
593 r#type = "text/css",
594 href = "/style/ip.css"
595 ),
596 ),
597 body!(
598 geo_comment,
599 div!(
600 class = "card",
601 div!(
602 class = "logo",
603 div!("π¦"),
604 div!(a!(href = "https://ramaproxy.org", "γ©γ")),
605 ),
606 div!(
607 class = "panel",
608 role = "region",
609 "aria-label" = "ip panel",
610 div!(class = "muted", "Your public ip"),
611 div!(id = "ip", class = "ip", code!(ip.to_string())),
612 div!(
613 class = "controls",
614 button!(
615 id = "copyBtn",
616 class = "primary",
617 title = "Copy ip to clipboard",
618 "π Copy IP",
619 ),
620 ),
621 ),
622 geo_panel,
623 script!(src = "/script/ip.js"),
624 )
625 ),
626 )
627}
628
629#[cfg(test)]
630mod render_html_page_tests {
631 use super::*;
632 use crate::http::protocols::html::IntoHtml as _;
633 use std::net::Ipv4Addr;
634
635 #[test]
640 fn render_html_page_embeds_ip_safely() {
641 let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
642 let out = render_html_page(ip, None, &[]).into_string();
643 assert!(out.starts_with("<!DOCTYPE html><html lang=\"en\">"));
644 assert!(out.contains("<title>Rama IP</title>"));
645 assert!(out.contains(r#"<div id="ip" class="ip"><code>127.0.0.1</code></div>"#));
646 assert!(out.contains(r#"id="copyBtn""#));
648 }
649
650 #[test]
653 fn render_html_page_emits_aria_label() {
654 let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1));
655 let out = render_html_page(ip, None, &[]).into_string();
656 assert!(out.contains(r#"aria-label="ip panel""#));
657 }
658
659 #[test]
665 fn render_html_page_uses_external_assets() {
666 let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
667 let out = render_html_page(ip, None, &[]).into_string();
668 assert!(
669 !out.contains("<style>") && !out.contains("<style "),
670 "IP page must not embed inline <style>; CSP blocks it"
671 );
672 assert!(
675 !out.contains("<script>"),
676 "IP page must not embed inline <script>; CSP blocks it"
677 );
678 assert!(
679 out.contains(r#"<link rel="stylesheet" type="text/css" href="/style/ip.css">"#),
680 "IP page must link to /style/ip.css",
681 );
682 assert!(
683 out.contains(r#"<script src="/script/ip.js">"#),
684 "IP page must source /script/ip.js",
685 );
686 }
687
688 #[test]
691 fn render_html_page_renders_geo_panel() {
692 use crate::geo::Country;
693 use crate::net::address::ip::geo::{GeoLocation, IpGeoInfo, IpGeoSourceResult};
694 let ip = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
695 let loc = GeoLocation {
696 country: Some(Country::Belgium),
697 ..Default::default()
698 };
699 let info = IpGeoInfo {
700 ip,
701 location: loc.clone(),
702 by_source: vec![IpGeoSourceResult {
703 label: "geolite2".into(),
704 location: loc,
705 }],
706 };
707 let notices = ["This product includes GeoLite2 data created by MaxMind"];
708 let out = render_html_page(ip, Some(&info), ¬ices).into_string();
709 assert!(out.contains("Geolocation"), "geo panel title missing");
710 assert!(out.contains("Belgium"), "resolved country missing");
711 assert!(out.contains("geolite2"), "per-source label missing");
712 assert!(
714 out.contains("<!-- This product includes GeoLite2"),
715 "attribution comment missing"
716 );
717
718 let plain = render_html_page(ip, None, &[]).into_string();
720 assert!(!plain.contains("Geolocation"));
721 assert!(!plain.contains("<!--"));
722 }
723}