upstream_rs.rs (7239B)
1 /* 2 This file is part of Paivana. 3 Copyright (C) 2026 Taler Systems SA 4 5 Paivana is free software; you can redistribute it and/or 6 modify it under the terms of the GNU General Public License 7 as published by the Free Software Foundation; either version 8 3, or (at your option) any later version. 9 10 Paivana is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 GNU General Public License for more details. 14 15 You should have received a copy of the GNU General Public 16 License along with Paivana; see the file COPYING. If not, 17 write to the Free Software Foundation, Inc., 51 Franklin 18 Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 */ 20 21 // upstream_rs: Rust-based upstream HTTP server used by the 22 // paivana reverse-proxy tests. Implements the same small set 23 // of canned endpoints as upstream_mhd.c, using only the std 24 // library so it can be built with just `rustc`. 25 26 use std::env; 27 use std::io::{BufRead, BufReader, Read, Write}; 28 use std::net::{TcpListener, TcpStream}; 29 use std::thread; 30 use std::time::Duration; 31 32 const UPSTREAM: &str = "rs"; 33 34 struct Request { 35 method: String, 36 path: String, 37 headers: Vec<(String, String)>, 38 body: Vec<u8>, 39 } 40 41 fn parse_request(stream: &mut TcpStream) -> Option<Request> { 42 let mut br = BufReader::new(stream); 43 let mut line = String::new(); 44 if br.read_line(&mut line).ok()? == 0 { 45 return None; 46 } 47 let mut parts = line.trim_end().splitn(3, ' '); 48 let method = parts.next()?.to_string(); 49 let path = parts.next()?.to_string(); 50 let mut headers = Vec::new(); 51 loop { 52 let mut h = String::new(); 53 if br.read_line(&mut h).ok()? == 0 { 54 break; 55 } 56 let t = h.trim_end(); 57 if t.is_empty() { 58 break; 59 } 60 if let Some(idx) = t.find(':') { 61 let k = t[..idx].trim().to_string(); 62 let v = t[idx + 1..].trim().to_string(); 63 headers.push((k, v)); 64 } 65 } 66 // Read body if there's a Content-Length 67 let cl: usize = headers 68 .iter() 69 .find(|(k, _)| k.eq_ignore_ascii_case("Content-Length")) 70 .and_then(|(_, v)| v.parse().ok()) 71 .unwrap_or(0); 72 let mut body = vec![0u8; cl]; 73 if cl > 0 { 74 if br.read_exact(&mut body).is_err() { 75 return None; 76 } 77 } 78 Some(Request { 79 method, 80 path, 81 headers, 82 body, 83 }) 84 } 85 86 fn send_response(stream: &mut TcpStream, code: u16, reason: &str, 87 content_type: &str, body: &[u8], extra_headers: &[(&str, &str)]) { 88 let mut head = format!( 89 "HTTP/1.1 {} {}\r\nX-Upstream: {}\r\nContent-Type: {}\r\nContent-Length: {}\r\n", 90 code, 91 reason, 92 UPSTREAM, 93 content_type, 94 body.len() 95 ); 96 for (k, v) in extra_headers { 97 head.push_str(&format!("{}: {}\r\n", k, v)); 98 } 99 head.push_str("Connection: keep-alive\r\n\r\n"); 100 let _ = stream.write_all(head.as_bytes()); 101 let _ = stream.write_all(body); 102 } 103 104 fn handle(req: &Request, stream: &mut TcpStream) { 105 if req.method == "OPTIONS" { 106 send_response( 107 stream, 204, "No Content", "text/plain", &[], 108 &[("Allow", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS")], 109 ); 110 return; 111 } 112 if req.path == "/hello" && (req.method == "GET" || req.method == "HEAD") { 113 let body = format!("Hello from {}\n", UPSTREAM); 114 let b: &[u8] = if req.method == "HEAD" { &[] } else { body.as_bytes() }; 115 // For HEAD, still advertise correct Content-Length of the would-be body. 116 if req.method == "HEAD" { 117 let head = format!( 118 "HTTP/1.1 200 OK\r\nX-Upstream: {}\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: keep-alive\r\n\r\n", 119 UPSTREAM, 120 body.len() 121 ); 122 let _ = stream.write_all(head.as_bytes()); 123 } else { 124 send_response(stream, 200, "OK", "text/plain", b, &[]); 125 } 126 return; 127 } 128 if let Some(rest) = req.path.strip_prefix("/status/") { 129 let code: u16 = rest.parse().unwrap_or(500); 130 let code = if !(100..=599).contains(&code) { 500 } else { code }; 131 let body = format!("status {}\n", code); 132 send_response(stream, code, "Status", "text/plain", body.as_bytes(), &[]); 133 return; 134 } 135 if let Some(rest) = req.path.strip_prefix("/large/") { 136 let n: usize = rest.parse().unwrap_or(0).min(10 * 1024 * 1024); 137 let mut buf = Vec::with_capacity(n); 138 for i in 0..n { 139 buf.push(b'A' + ((i % 26) as u8)); 140 } 141 send_response(stream, 200, "OK", "application/octet-stream", &buf, &[]); 142 return; 143 } 144 if let Some(rest) = req.path.strip_prefix("/slow/") { 145 let ms: u64 = rest.parse().unwrap_or(0).min(30000); 146 thread::sleep(Duration::from_millis(ms)); 147 send_response(stream, 200, "OK", "text/plain", b"slept\n", &[]); 148 return; 149 } 150 if req.path == "/echo-headers" && req.method == "GET" { 151 let mut b = String::new(); 152 for (k, v) in &req.headers { 153 b.push_str(&format!("{}: {}\n", k, v)); 154 } 155 send_response(stream, 200, "OK", "text/plain", b.as_bytes(), &[]); 156 return; 157 } 158 if req.path == "/echo" && req.method == "POST" { 159 send_response(stream, 200, "OK", "application/octet-stream", &req.body, &[]); 160 return; 161 } 162 if req.path == "/upload" && req.method == "POST" { 163 let b = format!("Received {} bytes\n", req.body.len()); 164 send_response(stream, 200, "OK", "text/plain", b.as_bytes(), &[]); 165 return; 166 } 167 if req.path == "/put" && req.method == "PUT" { 168 let b = format!("PUT received {}\n", req.body.len()); 169 send_response(stream, 200, "OK", "text/plain", b.as_bytes(), &[]); 170 return; 171 } 172 if req.path == "/patch" && req.method == "PATCH" { 173 let b = format!("PATCH received {}\n", req.body.len()); 174 send_response(stream, 200, "OK", "text/plain", b.as_bytes(), &[]); 175 return; 176 } 177 if req.path.starts_with("/item") && req.method == "DELETE" { 178 send_response(stream, 204, "No Content", "text/plain", &[], &[]); 179 return; 180 } 181 send_response(stream, 404, "Not Found", "text/plain", b"not found\n", &[]); 182 } 183 184 fn client_loop(mut stream: TcpStream) { 185 // We can't easily loop keep-alive with our BufReader pattern without 186 // ownership gymnastics; handle one request per connection. paivana 187 // opens fresh connections to us, so this is fine. 188 if let Some(req) = parse_request(&mut stream) { 189 handle(&req, &mut stream); 190 } 191 } 192 193 fn main() { 194 let port: u16 = env::args() 195 .nth(1) 196 .and_then(|s| s.parse().ok()) 197 .unwrap_or(8404); 198 let listener = TcpListener::bind(("0.0.0.0", port)).expect("bind failed"); 199 eprintln!("upstream_rs listening on port {}", port); 200 for stream in listener.incoming() { 201 match stream { 202 Ok(s) => { 203 thread::spawn(move || client_loop(s)); 204 } 205 Err(_) => continue, 206 } 207 } 208 }