diff options
author | Dennis Eriksen <d@ennis.no> | 2023-06-29 22:10:08 +0200 |
---|---|---|
committer | Dennis Eriksen <d@ennis.no> | 2023-06-29 22:10:08 +0200 |
commit | 8049367f4349662c3952a53144b52704a1b0ec93 (patch) | |
tree | 598dd1b2e100256d769a0c9ba95accaa6f168d25 /src | |
download | purl-rs-8049367f4349662c3952a53144b52704a1b0ec93.tar.gz |
initial commit - it sort of works a bit
Diffstat (limited to 'src')
-rw-r--r-- | src/cgidebug.rs | 81 | ||||
-rw-r--r-- | src/main.rs | 165 |
2 files changed, 246 insertions, 0 deletions
diff --git a/src/cgidebug.rs b/src/cgidebug.rs new file mode 100644 index 0000000..afdc7ca --- /dev/null +++ b/src/cgidebug.rs @@ -0,0 +1,81 @@ +use std::io::Write; +use std::env; +use std::collections::btree_map::BTreeMap; + +fn write_stderr( msg : String ) { + let mut stderr = std::io::stderr(); + write!(&mut stderr, "{}", msg).unwrap(); +} + +fn write_stderr_s( msg : &str ) { + write_stderr( msg.to_string() ); +} + +fn write_stdout( msg : String ) { + let mut stdout = std::io::stdout(); + write!(&mut stdout, "{}", msg).unwrap(); +} + +fn write_stdout_s( msg : &str ) { + write_stdout( msg.to_string() ); +} + +fn html_escape( msg : String ) -> String { + let mut copy : String = String::with_capacity( msg.len() ); + + for thechar in msg.chars() { + if thechar == '&' { + copy.push_str( "&" ); + } else if thechar == '<' { + copy.push_str( "<" ); + } else if thechar == '>' { + copy.push_str( ">" ); + } else if thechar == '\"' { + copy.push_str( """ ); + } else { + copy.push( thechar ); + } + } + + return copy; +} + +fn main() { + write_stdout_s( "Status: 301 Moved Permanently\n" ); + write_stdout_s( "Location: https://www.vg.no\n" ); + write_stdout_s( "Content-type: text/html\n" ); + write_stdout_s( "\n" ); + write_stdout_s( "<html>\n" ); + write_stdout_s( " <head>\n" ); + write_stdout_s( " <title>Rust CGI Test</title>\n" ); + write_stdout_s( " <style type=\"text/css\">\n" ); + write_stdout_s( " td { border:1px solid black; }\n" ); + write_stdout_s( " td { font-family:monospace; }\n" ); + write_stdout_s( " table { border-collapse:collapse; }\n" ); + write_stdout_s( " </style>\n" ); + write_stdout_s( " </head>\n" ); + write_stdout_s( " <body>\n" ); + write_stdout_s( " <h1>Environment</h1>\n" ); + write_stdout_s( " <table>\n" ); + write_stdout_s( " <tr><th>Key</th><th>Value</th></tr>\n" ); + + // copy environment into a BTreeMap which is sorted + let mut sortedmap : BTreeMap<String,String> = BTreeMap::new(); + for (key, value) in env::vars() { + sortedmap.insert( key, value ); + } + + // output environment into HTML table + for (key, value) in sortedmap { + write_stdout( + format!( + " <tr><td>{}</td><td>{}</td></tr>\n", + html_escape( key ), + html_escape( value ) + ) + ); + } + write_stdout_s( " </table>\n" ); + write_stdout_s( " </body>\n" ); + write_stdout_s( "</html>\n" ); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7a60081 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,165 @@ +//use std::io::Write; +use dumb_cgi::{Request, EmptyResponse, Query}; +use postgres::{Client, NoTls}; +use std::process::exit; +use regex::Regex; +use dotenv; +use url::Url; +use rand::{thread_rng, Rng}; +use rand::distributions::Alphanumeric; + + +// Get URL from database +fn get_url(dburl:String, short: &str) -> Result<String, postgres::Error> { + // Connect to db + let mut client = Client::connect(&dburl, NoTls)?; + + let row = client.query_one("SELECT url FROM shorts WHERE short = $1 LIMIT 1", &[&short])?; + + if row.len() == 1 { + client.execute("UPDATE shorts SET count = count + 1, last_visited = now() WHERE short = $1;", &[&short])?; + } + client.close()?; + + let url: String = row.get("url"); + + Ok(url) +} + +fn insert_short(dburl:String, url: &str, short: &str, user: &str) -> Result<u64, postgres::Error> { + let mut client = Client::connect(&dburl, NoTls)?; + + let n = client.execute("INSERT INTO shorts (url, short, created_by) VALUES ($1, $2, $3);", &[&url, &short, &user])?; + client.close()?; + Ok(n) +} +// Do the redirect +// return std::io::Result<()> so we can use ? - +// https://doc.rust-lang.org/std/result/#the-question-mark-operator- +fn redirect(code:u16, url:&str) { + EmptyResponse::new(code) + .with_header("Location", url) + .with_content_type("text/plain") + .with_body( + format!("Redirecting to {}\n", url)) + .respond().unwrap(); + exit(0); +} + +fn error(code:u16, msg:&str) { + EmptyResponse::new(code) + .with_content_type("text/plain") + .with_body( + format!("{} {}\n", code, msg)) + .respond().unwrap(); + exit(0); +} + +// Print form +fn print_form() { + let html = r#"<html> +<head> + <title>PURL</title> +</head> +<body> + <form method="post" action="/purl.cgi" enctype="multipart/form-data" name="main"> + Username: $user<br> + URL to shorten: <input type="text" name="url" size="50"><br> + Custom short: <input type="text" name="short"><br> + <input type="hidden" name="form" value="html"> + <input type="submit" value="Submit"> + </form> + +</body> +</html> +HTML +"#; + + EmptyResponse::new(200) + .with_content_type("text/html") + .with_body(html) + .respond().unwrap(); + exit(0); +} + + +// Generate random short +fn gen_short() -> String { + let rand_string: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(thread_rng().gen_range(2..6)) + .map(char::from) + .collect(); + + return rand_string; +} + + + +// Do the dirty +fn main() -> std::io::Result<()> { + // Get dburl from .env-file + let dburl = dotenv::var("DATABASE_URL").unwrap(); + let prefix = dotenv::var("PREFIX").unwrap(); + + // Gather all request data from the environment and stdin. + let req = Request::new().unwrap(); + + // Find out what action we're performing + let action:&str = req.var("ACTION").unwrap_or("none"); + + match action { + "form" => print_form(), + + "redirect" => { + // get DOCUMENT_URI + let docuri:&str = req.var("DOCUMENT_URI").unwrap_or("none"); + // Trim / and + from start + let docuri = docuri.trim_start_matches(&['/', '+']); + + // Make SURE docuri is a valid short + let shortregex = Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap(); + if !shortregex.is_match(docuri) { + error(400, "Bad Request"); + } + + // Fetch URL from postgres, and redirect or return 404 + match get_url(dburl, docuri) { + Ok(url) => redirect(301, &url), + Err(_e) => error(404, "Not Found"), + }; + }, + + "create" => { + match req.query() { + Query::None => redirect(303, "/purl.cgi"), + Query::Some(map) => { + if map.iter().count() != 1 { + error(400, "Bad Request\n\nIncorrect number of query items"); + } + let url:&str = &map["url"]; + if !Url::parse(url).is_ok() { + error(400, "Bad Request\n\nInvalid url"); + } + + let user:&str = req.var("REMOTE_USER").unwrap_or("none"); + let short:&str = &gen_short(); + let res:String = format!("{}{}", prefix, short); + if insert_short(dburl, url, short, user).unwrap() > 0 { + error(200, &res); + } else { + error(400, "Bad Request\n\nDunno wtf happened"); + } + exit(0); + + }, + Query::Err(_e) => error(400,"Bad Request\n\nError reading query string"), + } + }, + + _ => error(400, "Bad Request"), + } + + + Ok(()) +} |