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,
16 error::{ErrorExt as _, extra::OpaqueError},
17 extensions::ExtensionsRef,
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::forwarded::Forwarded,
35 net::stream::{SocketInfo, layer::http::BodyLimitLayer},
36 proxy::haproxy::server::HaProxyLayer,
37 rt::Executor,
38 tcp::TcpStream,
39 telemetry::tracing,
40};
41
42#[cfg(all(feature = "rustls", not(feature = "boring")))]
43use crate::tls::rustls::server::{TlsAcceptorData, TlsAcceptorLayer};
44
45#[cfg(any(feature = "rustls", feature = "boring"))]
46use crate::http::headers::StrictTransportSecurity;
47
48#[cfg(feature = "boring")]
49use crate::{
50 net::tls::server::ServerConfig,
51 tls::boring::server::{TlsAcceptorData, TlsAcceptorLayer},
52};
53
54#[cfg(feature = "boring")]
55type TlsConfig = ServerConfig;
56
57#[cfg(all(feature = "rustls", not(feature = "boring")))]
58type TlsConfig = TlsAcceptorData;
59
60use std::{convert::Infallible, marker::PhantomData, net::IpAddr, sync::Arc, time::Duration};
61use tokio::io::AsyncWriteExt;
62
63#[derive(Debug, Clone)]
64pub struct IpServiceBuilder<M> {
67 #[cfg(any(feature = "rustls", feature = "boring"))]
68 tls_server_config: Option<TlsConfig>,
69 concurrent_limit: usize,
70 timeout: Duration,
71 forward: Option<ForwardKind>,
72 _mode: PhantomData<fn(M)>,
73}
74
75impl IpServiceBuilder<mode::Http> {
76 #[must_use]
78 pub fn http() -> Self {
79 Self {
80 #[cfg(any(feature = "rustls", feature = "boring"))]
81 tls_server_config: None,
82 concurrent_limit: 0,
83 timeout: Duration::ZERO,
84 forward: None,
85 _mode: PhantomData,
86 }
87 }
88}
89
90impl IpServiceBuilder<mode::Transport> {
91 #[must_use]
93 pub fn tcp() -> Self {
94 Self {
95 #[cfg(any(feature = "rustls", feature = "boring"))]
96 tls_server_config: None,
97 concurrent_limit: 0,
98 timeout: Duration::ZERO,
99 forward: None,
100 _mode: PhantomData,
101 }
102 }
103}
104
105impl<M> IpServiceBuilder<M> {
106 crate::utils::macros::generate_set_and_with! {
107 #[must_use]
109 pub fn concurrent(mut self, limit: usize) -> Self {
110 self.concurrent_limit = limit;
111 self
112 }
113 }
114
115 crate::utils::macros::generate_set_and_with! {
116 #[must_use]
118 pub fn timeout(mut self, timeout: Duration) -> Self {
119 self.timeout = timeout;
120 self
121 }
122 }
123
124 crate::utils::macros::generate_set_and_with! {
125 #[must_use]
137 pub fn forward(mut self, maybe_kind: Option<ForwardKind>) -> Self {
138 self.forward = maybe_kind;
139 self
140 }
141 }
142
143 crate::utils::macros::generate_set_and_with! {
144 #[cfg(any(feature = "rustls", feature = "boring"))]
145 pub fn tls_server_config(mut self, cfg: Option<TlsConfig>) -> Self {
148 self.tls_server_config = cfg;
149 self
150 }
151 }
152}
153
154impl IpServiceBuilder<mode::Http> {
155 #[allow(unused_mut)]
156 #[inline]
157 pub fn build(
159 mut self,
160 executor: Executor,
161 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
162 #[cfg(all(feature = "rustls", not(feature = "boring")))]
163 let tls_cfg = self.tls_server_config.take();
164
165 #[cfg(feature = "boring")]
166 let tls_cfg: Option<TlsAcceptorData> = match self.tls_server_config.take() {
167 Some(cfg) => Some(cfg.try_into()?),
168 None => None,
169 };
170
171 #[cfg(any(feature = "rustls", feature = "boring"))]
172 {
173 let maybe_tls_acceptor_layer = tls_cfg.map(TlsAcceptorLayer::new);
174 self.build_http(executor, maybe_tls_acceptor_layer)
175 }
176
177 #[cfg(not(any(feature = "rustls", feature = "boring")))]
178 self.build_http(executor)
179 }
180}
181
182#[derive(Debug, Clone)]
183#[non_exhaustive]
184struct HttpIpService;
189
190impl Service<Request> for HttpIpService {
191 type Output = Response;
192 type Error = Infallible;
193
194 async fn serve(&self, req: Request) -> Result<Self::Output, Self::Error> {
195 let peer_ip = req
196 .extensions()
197 .get_ref::<Forwarded>()
198 .and_then(|f| f.client_ip())
199 .or_else(|| {
200 req.extensions()
201 .get_ref::<SocketInfo>()
202 .map(|s| s.peer_addr().ip_addr)
203 });
204
205 Ok(match peer_ip {
206 Some(ip) => match HttpBodyContentFormat::derive_from_req(&req) {
207 HttpBodyContentFormat::Txt => ip.to_string().into_response(),
208 HttpBodyContentFormat::Html => render_html_page(ip).into_response(),
209 HttpBodyContentFormat::Json => Json(serde_json::json!({
210 "ip": ip,
211 }))
212 .into_response(),
213 },
214 None => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
215 })
216 }
217}
218
219const IP_STYLE_CSS: &str = include_str!("ip.css");
223
224const IP_SCRIPT_JS: &str = include_str!("ip.js");
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
229enum HttpBodyContentFormat {
230 #[default]
231 Txt,
232 Html,
233 Json,
234}
235
236impl HttpBodyContentFormat {
237 fn derive_from_req(req: &Request) -> Self {
238 let Some(accept) = req.headers().typed_get::<Accept>() else {
239 return Self::default();
240 };
241 accept
242 .0
243 .iter()
244 .find_map(|qv| {
245 let r#type = qv.value.subtype();
246 if r#type == mime::JSON {
247 Some(Self::Json)
248 } else if r#type == mime::HTML {
249 Some(Self::Html)
250 } else if r#type == mime::TEXT {
251 Some(Self::Txt)
252 } else {
253 None
254 }
255 })
256 .unwrap_or_default()
257 }
258}
259
260#[derive(Debug, Clone)]
261#[non_exhaustive]
262struct TcpIpService;
264
265impl<Input> Service<Input> for TcpIpService
266where
267 Input: Io + Unpin + ExtensionsRef,
268{
269 type Output = ();
270 type Error = BoxError;
271
272 async fn serve(&self, stream: Input) -> Result<Self::Output, Self::Error> {
273 tracing::info!("connection received");
274 let peer_ip = stream
275 .extensions()
276 .get_ref::<Forwarded>()
277 .and_then(|f| f.client_ip())
278 .or_else(|| {
279 stream
280 .extensions()
281 .get_ref::<SocketInfo>()
282 .map(|s| s.peer_addr().ip_addr)
283 });
284 let Some(peer_ip) = peer_ip else {
285 tracing::error!("missing peer information");
286 return Ok(());
287 };
288
289 let mut stream = std::pin::pin!(stream);
290
291 match peer_ip {
292 std::net::IpAddr::V4(ip) => {
293 if let Err(err) = stream.write_all(&ip.octets()).await {
294 tracing::error!("error writing IPv4 of peer to peer: {}", err);
295 }
296 }
297 std::net::IpAddr::V6(ip) => {
298 if let Err(err) = stream.write_all(&ip.octets()).await {
299 tracing::error!("error writing IPv6 of peer to peer: {}", err);
300 }
301 }
302 };
303
304 Ok(())
305 }
306}
307
308impl IpServiceBuilder<mode::Transport> {
309 #[allow(unused_mut)]
310 #[inline]
311 pub fn build(
313 mut self,
314 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
315 #[cfg(all(feature = "rustls", not(feature = "boring")))]
316 let tls_cfg = self.tls_server_config.take();
317
318 #[cfg(feature = "boring")]
319 let tls_cfg: Option<TlsAcceptorData> = match self.tls_server_config.take() {
320 Some(cfg) => Some(cfg.try_into()?),
321 None => None,
322 };
323
324 #[cfg(any(feature = "rustls", feature = "boring"))]
325 {
326 let maybe_tls_acceptor_layer = tls_cfg.map(TlsAcceptorLayer::new);
327 self.build_tcp(maybe_tls_acceptor_layer)
328 }
329
330 #[cfg(not(any(feature = "rustls", feature = "boring")))]
331 self.build_tcp()
332 }
333}
334
335impl<M> IpServiceBuilder<M> {
336 fn build_tcp<S: Io + ExtensionsRef + Unpin + Sync>(
337 self,
338 #[cfg(any(feature = "rustls", feature = "boring"))] maybe_tls_accept_layer: Option<
339 TlsAcceptorLayer,
340 >,
341 ) -> Result<impl Service<S, Output = (), Error = Infallible>, BoxError> {
342 let tcp_forwarded_layer = match &self.forward {
343 None => None,
344 Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
345 Some(other) => {
346 return Err(OpaqueError::from_static_str(
347 "invalid forward kind for Transport mode",
348 )
349 .with_context_debug_field("kind", || other.clone()));
350 }
351 };
352
353 let tcp_service_builder = (
354 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
355 LimitLayer::new(if self.concurrent_limit > 0 {
356 Either::A(ConcurrentPolicy::max(self.concurrent_limit))
357 } else {
358 Either::B(UnlimitedPolicy::new())
359 }),
360 if !self.timeout.is_zero() {
361 TimeoutLayer::new(self.timeout)
362 } else {
363 TimeoutLayer::never()
364 },
365 tcp_forwarded_layer,
366 #[cfg(any(feature = "rustls", feature = "boring"))]
367 maybe_tls_accept_layer,
368 );
369
370 Ok(tcp_service_builder.into_layer(TcpIpService))
371 }
372
373 fn build_http<S: Io + Unpin + Sync + ExtensionsRef>(
374 self,
375 executor: Executor,
376 #[cfg(any(feature = "rustls", feature = "boring"))] maybe_tls_accept_layer: Option<
377 TlsAcceptorLayer,
378 >,
379 ) -> Result<impl Service<S, Output = (), Error = Infallible>, BoxError> {
380 let (tcp_forwarded_layer, http_forwarded_layer) = match &self.forward {
381 None => (None, None),
382 Some(ForwardKind::Forwarded) => {
383 (None, Some(Either7::A(GetForwardedHeaderLayer::forwarded())))
384 }
385 Some(ForwardKind::XForwardedFor) => (
386 None,
387 Some(Either7::B(GetForwardedHeaderLayer::x_forwarded_for())),
388 ),
389 Some(ForwardKind::XClientIp) => (
390 None,
391 Some(Either7::C(GetForwardedHeaderLayer::<XClientIp>::new())),
392 ),
393 Some(ForwardKind::ClientIp) => (
394 None,
395 Some(Either7::D(GetForwardedHeaderLayer::<ClientIp>::new())),
396 ),
397 Some(ForwardKind::XRealIp) => (
398 None,
399 Some(Either7::E(GetForwardedHeaderLayer::<XRealIp>::new())),
400 ),
401 Some(ForwardKind::CFConnectingIp) => (
402 None,
403 Some(Either7::F(GetForwardedHeaderLayer::<CFConnectingIp>::new())),
404 ),
405 Some(ForwardKind::TrueClientIp) => (
406 None,
407 Some(Either7::G(GetForwardedHeaderLayer::<TrueClientIp>::new())),
408 ),
409 Some(ForwardKind::HaProxy) => (Some(HaProxyLayer::default()), None),
410 };
411
412 #[cfg(any(feature = "rustls", feature = "boring"))]
413 let hsts_layer = maybe_tls_accept_layer.is_some().then(|| {
414 SetResponseHeaderLayer::if_not_present_typed(
415 StrictTransportSecurity::excluding_subdomains_for_max_seconds(31536000),
416 )
417 });
418
419 let tcp_service_builder = (
420 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
421 (self.concurrent_limit > 0)
422 .then(|| LimitLayer::new(ConcurrentPolicy::max(self.concurrent_limit))),
423 (!self.timeout.is_zero()).then(|| TimeoutLayer::new(self.timeout)),
424 tcp_forwarded_layer,
425 BodyLimitLayer::request_only(1024 * 1024),
427 #[cfg(any(feature = "rustls", feature = "boring"))]
428 maybe_tls_accept_layer,
429 );
430
431 let (csp_layer, nosniff_layer, referrer_layer, frame_layer) =
439 crate::cli::service::http_security::defence_in_depth_layer(
440 crate::cli::service::http_security::rama_html_csp(),
441 );
442
443 let router = crate::http::service::web::Router::new()
447 .with_get("/", HttpIpService)
448 .with_get("/style/ip.css", Css(IP_STYLE_CSS))
449 .with_get("/script/ip.js", Script(IP_SCRIPT_JS))
450 .with_not_found(async || Redirect::permanent("/"));
451
452 let http_service = (
453 TraceLayer::new_for_http(),
454 SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
455 AddRequiredResponseHeadersLayer::default(),
456 csp_layer,
457 nosniff_layer,
458 referrer_layer,
459 frame_layer,
460 ConsumeErrLayer::default(),
461 #[cfg(any(feature = "rustls", feature = "boring"))]
462 hsts_layer,
463 http_forwarded_layer,
464 )
465 .into_layer(router);
466
467 let http_service = Arc::new(http_service);
471 Ok(tcp_service_builder.into_layer(HttpServer::auto(executor).service(http_service)))
472 }
473}
474
475pub mod mode {
476 #[derive(Debug, Clone)]
479 #[non_exhaustive]
480 pub struct Http;
482
483 #[derive(Debug, Clone)]
484 #[non_exhaustive]
485 pub struct Transport;
487}
488
489fn render_html_page(ip: IpAddr) -> impl crate::http::html::IntoHtml + IntoResponse {
490 use crate::http::html::*;
491 html!(
492 lang = "en",
493 head!(
494 meta!(charset = "utf-8"),
495 meta!(
496 name = "viewport",
497 content = "width=device-width,initial-scale=1"
498 ),
499 link!(
500 rel = "icon",
501 href = PreEscaped(
502 "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>\
503 <text y='0.9em' font-size='90'>🦙</text></svg>"
504 ),
505 ),
506 title!("Rama IP"),
507 link!(
508 rel = "stylesheet",
509 r#type = "text/css",
510 href = "/style/ip.css"
511 ),
512 ),
513 body!(div!(
514 class = "card",
515 div!(
516 class = "logo",
517 div!("🦙"),
518 div!(a!(href = "https://ramaproxy.org", "ラマ")),
519 ),
520 div!(
521 class = "panel",
522 role = "region",
523 "aria-label" = "ip panel",
524 div!(class = "muted", "Your public ip"),
525 div!(id = "ip", class = "ip", code!(ip.to_string())),
526 div!(
527 class = "controls",
528 button!(
529 id = "copyBtn",
530 class = "primary",
531 title = "Copy ip to clipboard",
532 "📋 Copy IP",
533 ),
534 ),
535 ),
536 script!(src = "/script/ip.js"),
537 )),
538 )
539}
540
541#[cfg(test)]
542mod render_html_page_tests {
543 use super::*;
544 use crate::http::html::IntoHtml as _;
545 use std::net::Ipv4Addr;
546
547 #[test]
552 fn render_html_page_embeds_ip_safely() {
553 let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
554 let out = render_html_page(ip).into_string();
555 assert!(out.starts_with("<!DOCTYPE html><html lang=\"en\">"));
556 assert!(out.contains("<title>Rama IP</title>"));
557 assert!(out.contains(r#"<div id="ip" class="ip"><code>127.0.0.1</code></div>"#));
558 assert!(out.contains(r#"id="copyBtn""#));
560 }
561
562 #[test]
565 fn render_html_page_emits_aria_label() {
566 let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1));
567 let out = render_html_page(ip).into_string();
568 assert!(out.contains(r#"aria-label="ip panel""#));
569 }
570
571 #[test]
577 fn render_html_page_uses_external_assets() {
578 let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
579 let out = render_html_page(ip).into_string();
580 assert!(
581 !out.contains("<style>") && !out.contains("<style "),
582 "IP page must not embed inline <style>; CSP blocks it"
583 );
584 assert!(
587 !out.contains("<script>"),
588 "IP page must not embed inline <script>; CSP blocks it"
589 );
590 assert!(
591 out.contains(r#"<link rel="stylesheet" type="text/css" href="/style/ip.css">"#),
592 "IP page must link to /style/ip.css",
593 );
594 assert!(
595 out.contains(r#"<script src="/script/ip.js">"#),
596 "IP page must source /script/ip.js",
597 );
598 }
599}