Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_steel/src/srv/http.rs

5.7 KiB, 43 runs

created by r1870400018:10028, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
2//! Anthropic Claude
3
4/// Plaintext HTTP listener.
5///
6/// Steel's primary listener is HTTPS on port 443 (or a development
7/// equivalent). Some clients — notably browsers that do not default to
8/// HTTPS-first mode — need an HTTP listener on port 80 that unconditionally
9/// redirects to the HTTPS origin. This module provides that listener.
10///
11/// The redirect preserves the incoming `Host` header and request target
12/// so deep links continue to work after the protocol upgrade.
13
14use oxedyne_fe2o3_core::prelude::*;
15
16use std::net::SocketAddr;
17
18use tokio::{
19 io::{
20 AsyncReadExt,
21 AsyncWriteExt,
22 },
23 net::{
24 TcpListener,
25 TcpStream,
26 },
27};
28
29
30// Only the request line and the `Host` header are needed, so a small buffer is
31// plenty and bounds the damage from a malicious client.
32const MAX_REQUEST_BYTES: usize = 8192;
33
34
35/// Binds `server_address:port` and accepts plaintext HTTP connections,
36/// answering each with a 301 redirect to the HTTPS equivalent.
37///
38/// Loops forever, and is meant to be spawned as a background Tokio task
39/// alongside the main HTTPS accept loop. Per-connection errors are logged but do
40/// not terminate the listener.
41pub async fn run_redirect_listener(
42 server_address: String,
43 http_port: u16,
44 https_port: u16,
45)
46 -> Outcome<()>
47{
48 let ip: std::net::IpAddr = match server_address.parse() {
49 Ok(ip) => ip,
50 Err(e) => return Err(err!(e,
51 "Invalid server_address '{}' for plaintext HTTP listener.",
52 server_address;
53 Invalid, Input, Network)),
54 };
55 let addr = SocketAddr::new(ip, http_port);
56 let listener = res!(TcpListener::bind(&addr).await, IO, Network);
57 info!("Listening on: {} (plaintext HTTP, redirects to HTTPS)", addr);
58
59 loop {
60 let (stream, src_addr) = match listener.accept().await {
61 Ok(pair) => pair,
62 Err(e) => {
63 error!(err!(e,
64 "Plaintext HTTP accept aborted.";
65 IO, Network));
66 continue;
67 }
68 };
69 tokio::spawn(async move {
70 if let Err(e) = handle_redirect(stream, src_addr, https_port).await {
71 error!(err!(e,
72 "Plaintext HTTP connection from {} failed.", src_addr;
73 IO, Network));
74 }
75 });
76 }
77}
78
79/// Parses just enough of the request to extract the `Host` header and request
80/// target, then writes a 301 with a `Location` header pointing at the HTTPS
81/// equivalent.
82///
83/// A malformed request still gets a safe 301 to the root of whatever host could
84/// be identified, or an explanatory plain-text 400 if nothing could be salvaged.
85///
86/// Also used by the HTTPS listener when it detects plaintext traffic on the TLS
87/// port -- someone pasting `http://` into a browser that does not upgrade.
88pub async fn handle_redirect(
89 mut stream: TcpStream,
90 _src_addr: SocketAddr,
91 https_port: u16,
92)
93 -> Outcome<()>
94{
95 // Read until we see the end-of-headers marker or hit the size cap.
96 let mut buf = Vec::with_capacity(1024);
97 let mut tmp = [0u8; 1024];
98 loop {
99 if buf.len() >= MAX_REQUEST_BYTES {
100 break;
101 }
102 let n = match stream.read(&mut tmp).await {
103 Ok(0) => break,
104 Ok(n) => n,
105 Err(_) => break,
106 };
107 buf.extend_from_slice(&tmp[..n]);
108 if buf.windows(4).any(|w| w == b"\r\n\r\n") {
109 break;
110 }
111 }
112
113 // Parse request line and Host header.
114 let text = String::from_utf8_lossy(&buf);
115 let mut lines = text.split("\r\n");
116 let request_line = lines.next().unwrap_or("");
117 let mut parts = request_line.split_whitespace();
118 let _method = parts.next().unwrap_or("");
119 let target = parts.next().unwrap_or("/");
120
121 let mut host: Option<&str> = None;
122 for line in lines {
123 if line.is_empty() {
124 break;
125 }
126 if let Some(rest) = line.strip_prefix("Host:").or_else(|| line.strip_prefix("host:")) {
127 host = Some(rest.trim());
128 break;
129 }
130 }
131
132 let location = match host {
133 Some(h) if !h.is_empty() => {
134 // Strip any incoming port, we always redirect to the HTTPS port.
135 let host_only = match h.rfind(':') {
136 Some(i) => &h[..i],
137 None => h,
138 };
139 if https_port == 443 {
140 fmt!("https://{}{}", host_only, target)
141 } else {
142 fmt!("https://{}:{}{}", host_only, https_port, target)
143 }
144 }
145 _ => {
146 let body = "Bad Request: missing Host header.";
147 let response = fmt!(
148 "HTTP/1.1 400 Bad Request\r\n\
149 Connection: close\r\n\
150 Content-Type: text/plain; charset=utf-8\r\n\
151 Content-Length: {}\r\n\
152 \r\n\
153 {}",
154 body.len(), body,
155 );
156 let _ = stream.write_all(response.as_bytes()).await;
157 let _ = stream.shutdown().await;
158 return Ok(());
159 }
160 };
161
162 let body = fmt!("Redirecting to {}", location);
163 let response = fmt!(
164 "HTTP/1.1 301 Moved Permanently\r\n\
165 Location: {}\r\n\
166 Connection: close\r\n\
167 Content-Type: text/plain; charset=utf-8\r\n\
168 Content-Length: {}\r\n\
169 \r\n\
170 {}",
171 location, body.len(), body,
172 );
173 match stream.write_all(response.as_bytes()).await {
174 Ok(()) => (),
175 Err(e) => return Err(err!(e,
176 "Failed to write plaintext HTTP redirect response.";
177 IO, Network, Wire, Write)),
178 }
179 let _ = stream.shutdown().await;
180 Ok(())
181}