paivana

HTTP paywall reverse proxy
Log | Files | Refs | Submodules | README | LICENSE

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 }