aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDennis Eriksen <d@ennis.no>2023-06-29 22:10:08 +0200
committerDennis Eriksen <d@ennis.no>2023-06-29 22:10:08 +0200
commit8049367f4349662c3952a53144b52704a1b0ec93 (patch)
tree598dd1b2e100256d769a0c9ba95accaa6f168d25 /src
downloadpurl-rs-8049367f4349662c3952a53144b52704a1b0ec93.tar.gz
initial commit - it sort of works a bit
Diffstat (limited to 'src')
-rw-r--r--src/cgidebug.rs81
-rw-r--r--src/main.rs165
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( "&amp;" );
+ } else if thechar == '<' {
+ copy.push_str( "&lt;" );
+ } else if thechar == '>' {
+ copy.push_str( "&gt;" );
+ } else if thechar == '\"' {
+ copy.push_str( "&quot;" );
+ } 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(())
+}