Initial commit.

This commit is contained in:
2026-05-09 21:49:10 +01:00
commit 79ed58ee23
5 changed files with 320 additions and 0 deletions

116
src/main.rs Normal file
View File

@@ -0,0 +1,116 @@
use {
std::{collections::HashMap, env, sync::Arc},
tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufStream},
net::{TcpListener, TcpStream},
task::spawn_blocking,
},
};
enum ResponseCode {
BadRequest,
Forbidden,
NotFound,
}
async fn simple_response(mut socket: BufStream<TcpStream>, rc: ResponseCode) {
let rc = match rc {
ResponseCode::BadRequest => "400 Bad Request",
ResponseCode::Forbidden => "403 Forbidden",
ResponseCode::NotFound => "404 Not Found",
};
let content_length = rc.len();
let response = format!(
"HTTP/1.1 {rc}\r\nContent-Type: text/plaintext; charset=utf-8\r\nContent-Length: {content_length}\r\nX-Served-By: urls-txt\r\n\r\n{rc}"
);
if let Err(e) = socket.write(response.as_bytes()).await {
println!("process_request: failed: error writing response: {e}");
};
if let Err(e) = socket.flush().await {
println!("process_request: failed: error flushing socket: {e}");
};
}
async fn process_socket(mut socket: BufStream<TcpStream>, urlmap: Arc<HashMap<String, String>>) {
let mut request_line = String::default();
if let Err(e) = socket.read_line(&mut request_line).await {
println!("process_request: failed to read line: {e}");
return;
}
let mut request_line = request_line.split(' ');
let (Some(http_method), Some(path)) = (request_line.next(), request_line.next()) else {
println!("process_request: malformed request");
simple_response(socket, ResponseCode::BadRequest).await;
return;
};
if http_method != "GET" {
println!("process_request: forbidden method");
simple_response(socket, ResponseCode::Forbidden).await;
return;
}
let Some(location) = urlmap.get(path) else {
println!("process_request: not found");
simple_response(socket, ResponseCode::NotFound).await;
return;
};
let content = format!("<a href=\"{location}\">Found</a>\r\n");
let content_length = content.len();
let response = format!(
"HTTP/1.1 302 Found\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {content_length}\r\nLocation: {location}\r\nX-Served-By: urls-txt\r\n\r\n{content}"
);
if let Err(e) = socket.write(response.as_bytes()).await {
println!("process_request: failed: error writing response: {e}");
return;
};
if let Err(e) = socket.flush().await {
println!("process_request: failed: error flushing socket: {e}");
};
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("USAGE: {} <urls.txt>", args[0]);
Err("Incorrect arguments")?
}
let config = spawn_blocking(|| std::fs::read_to_string("urls.txt")).await??;
let mut urlmap = HashMap::new();
for url_pair in config.split('\n') {
if url_pair.len() == 0 {
continue;
}
if url_pair.chars().next().expect("String is longer than 1 character") != '/' {
continue;
}
let mut split = url_pair.split_whitespace();
let (Some(slug), Some(redirect)) = (split.next(), split.next()) else {
Err(format!("Unable to parse line: '{}'", url_pair))?
};
urlmap.insert(slug.to_string(), redirect.to_string());
}
let urlmap = Arc::new(urlmap);
println!("Starting TCP Listener...");
let listener = TcpListener::bind("127.0.0.1:3000").await?;
loop {
let (socket, _addr) = listener.accept().await?;
let urlmap = urlmap.clone();
tokio::spawn(async move {
process_socket(BufStream::new(socket), urlmap).await;
});
}
}