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:&str, short:&str, update:bool) -> 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 && update {
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)
}
fn status<'a>(code:u16) -> &'a str {
// I've only implemented statuscodes I *might* use
return match code {
200 => "OK",
201 => "Created",
202 => "Accepted",
204 => "No Content",
301 => "Moved Permanently",
302 => "Found",
304 => "Not Modified",
307 => "Temporary Redirect",
308 => "Permanent Redirect",
400 => "Bad Request",
401 => "Unauthorized",
402 => "Payment Required",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
406 => "Not Acceptableo",
410 => "Gone",
414 => "URI Too Long",
500 => "Internal Server Error",
501 => "Not Implemented",
503 => "Service Unavailable",
508 => "Loop Detected",
_ => "Not Found",
};
}
// Do the redirect
fn respond(code:u16, body:&str) {
let mut r = EmptyResponse::new(code)
.with_content_type("text/plain");
if code >= 300 {
write!(&mut r, "{} {}\n\n", code, status(code)).unwrap();
}
if (300..399).contains(&code) && Url::parse(body).is_ok() {
r.add_header("Location", body);
}
write!(&mut r, "{}\n", body).unwrap();
r.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
// return std::io::Result<()> so we can use ? -
// https://doc.rust-lang.org/std/result/#the-question-mark-operator-
fn main() -> std::io::Result<()> {
// short = ID for shortened url
// url = url to be shortened / that has been shortened
// shorturl = full shortened url
// shortprefix = part of url that comes before short id
// docuri = DOCUMENT_URI from http request. This is everything after the domain.
// Example: "/hey" in "https://example.com/hey"
// Get dburl from .env-file
let dburl = dotenv::var("DATABASE_URL").unwrap();
let shortprefix = dotenv::var("SHORTPREFIX").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) {
respond(400, "Bad Request");
}
// Fetch URL from postgres, and redirect or return 404
match get_url(&dburl, docuri, true) {
Ok(url) => respond(301, &url),
Err(_e) => respond(404, "Not Found"),
};
},
"create" => {
match req.query() {
Query::None => respond(303, "/purl.cgi"),
Query::Some(map) => {
if map.iter().count() != 1 {
respond(400, "Bad Request\n\nIncorrect number of query items");
}
let url:&str = &map["url"];
if !Url::parse(url).is_ok() {
respond(400, "Bad Request\n\nInvalid url");
}
let user:&str = req.var("REMOTE_USER").unwrap_or("none");
let mut short:String = gen_short();
for i in 1..5 {
match get_url(&dburl, &short, false) {
Ok(_url) => short = gen_short(), // If Ok, then short is already in
// use. Try a new one.
Err(_e) => break, // we assume that we found a unique short if get_url
// returns an error
}
// Throw error if we couldn't create a unique short in fire tries
if i == 5 { respond(500, "Could not find unique short"); }
}
match insert_short(dburl, url, &short, user) {
Ok(_v) => respond(200, &format!("{}{}", shortprefix, short)),
Err(_e) => respond(400, "looool"),
};
//if n == 1 {
// respond(200, &res);
//} else {
// respond(400, "Bad Request\n\nDunno wtf happened");
//}
exit(0);
},
Query::Err(_e) => respond(400,"Bad Request\n\nError reading query string"),
}
},
_ => respond(400, "Bad Request"),
}
Ok(())
}