I'm a Rust newbie. As a pet project, I decided to implement a simple multi-threaded HTTPS proxy server in Rust which uses the CONNECT protocol. I have tested the code below using my browser.
use std::io::prelude::*; // Contains the read/write traits
use std::net::{TcpListener, TcpStream};
use std::str;
use std::io;
use std::thread;
const PORT: i32 = 5000;
// Function to handle connection
fn handle_client(mut stream: TcpStream) {
println!("[*] Received connection request from {:?}", stream);
// Read the CONNECT request's bytes into the buffer
let mut buf = [0; 4096];
// Read the bytes from the stream
let nbytes = match stream.read(&mut buf) {
Ok(n) => n,
Err(_) => return () // early return if an error
};
println!("[*] Received {} bytes of data", nbytes);
// Convert the request to a string
let req : &str = match str::from_utf8(&buf) {
Ok(s) => s,
Err(_) => return () // early return if an error
};
println!("[*] Received request {}", req);
// Split the text on whitespace and get the hostname
let website : &str = req.split_whitespace().collect::<Vec<_>>()[1];
println!("[*] Connecting to {}", website);
// Open a TCP connection to the website
let mut tunnel = match TcpStream::connect(website) {
Ok(t) => t,
Err(_) => return ()
};
// Send an ack to the client
match stream.write_all(b"HTTP/1.1 200 Connection established\r\n\r\n") {
Ok(_) => (),
Err(_) => return ()
};
// Set both sockets to nonblocking mode
match stream.set_nonblocking(true) {
Ok(()) => (),
Err(_) => return ()
};
match tunnel.set_nonblocking(true) {
Ok(()) => (),
Err(_) => return ()
}
let mut stream_buf = [0u8; 4096]; // Buffer containing data received from stream
let mut tunnel_buf = [0u8; 4096]; // Buffer containing data received from tunnel
let mut stream_nbytes = 0usize; // The number of bytes pending in stream_buf to be written to tunnel
let mut tunnel_nbytes = 0usize; // The number of bytes pending in tunnel_buf to be written to stream
// Keep copying data back and forth
loop {
// Read data from stream to be sent to tunnel -- only read if stream_buf is empty
if stream_nbytes == 0 {
stream_nbytes = match stream.read(&mut stream_buf) {
Ok(0) => return (), // Socket closed
Ok(n) => n,
Err(e) if e.kind() == io::ErrorKind::WouldBlock => 0, // If there is no data, return 0 bytes written
Err(_) => return ()
};
}
// Read data from tunnel to be sent to stream -- only read if tunnel_buf is empty
if tunnel_nbytes == 0 {
tunnel_nbytes = match tunnel.read(&mut tunnel_buf) {
Ok(0) => return (), // Socket closed
Ok(n) => n,
Err(e) if e.kind() == io::ErrorKind::WouldBlock => 0, // If there is no data, return 0 bytes written
Err(_) => return ()
};
}
// Write data from stream to tunnel
if stream_nbytes > 0 {
// Pass the slice corresponding to first `stream_nbytes`
match tunnel.write(&mut stream_buf[0..stream_nbytes]) {
Ok(0) => return (), // Socket closed
Ok(n) if n == stream_nbytes => stream_nbytes = 0, // If we get equal
Ok(_) => { println!("Cannot write partially :("); return () }, // No support for partial nbytes
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (), // Write the bytes in later
Err(_) => return ()
}
}
// Write data from tunnel to stream
if tunnel_nbytes > 0 {
// Pass the slice corresponding to first `stream_nbytes`
match stream.write(&mut tunnel_buf[0..tunnel_nbytes]) {
Ok(0) => return (), // Socket closed
Ok(n) if n == tunnel_nbytes => tunnel_nbytes = 0, // If we get equal
Ok(_) => { println!("Cannot write partially :("); return () }, // No support for partial nbytes
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (), // Write the bytes in later
Err(_) => return ()
}
}
}
}
fn main() -> std::io::Result<()> {
// Create a server
let local_addr = format!("localhost:{}", PORT);
let server = TcpListener::bind(local_addr)?;
println!("[*] Listening on port {}", PORT);
// Keep spinning and spawn threads for any incoming connections
for stream_result in server.incoming() {
match stream_result {
Ok(stream) => thread::spawn(move || handle_client(stream)), // Spawn a new thread, ignore the return value because we don't need to join threads
_ => continue
};
}
Ok(())
}
I am looking for critique on the following facets of my code -
- Is this idiomatic Rust? It feels quite verbose and convoluted when compared to implementations in other languages. I'm wondering if that is just a feature of Rust or I'm missing something. Is my use of Rust primitives (like slices, arrays, etc.) optimal? Can I refactor the code?
- Is there a better way to handle errors?
- Can I improve the performance of the code? Are there better alternatives than threading which are not too complicated to implement?
- Can I write better comments and improve readability?
- Can I improve logging?
Feel free to point out anything else that could have been done better.