aboutsummaryrefslogtreecommitdiff
path: root/src/service.rs
blob: b7b204c60a1afbfa6359846b94f2874864c3a9e6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// extern
use anyhow::{Context, Result};
use hyper::header::{HeaderValue, CONTENT_TYPE};
use hyper::{Body, Method, Request, Response, StatusCode};
use log::{error, info, trace};
use multer::Multipart;
use nanoid::nanoid;
use rusqlite::{params, Connection};
use url::form_urlencoded;

// std
use std::collections::HashMap;

fn get_host(req: &Request<Body>) -> Option<HeaderValue> {
    let host = req.headers().get("host").map(|h| h.clone())?;
    return Some(host);
}

fn welcome(req: Request<Body>) -> Response<Body> {
    let _h = get_host(&req);
    let host = _h.as_ref().map(|h| h.as_bytes()).unwrap_or(b"");
    let text = format!(
        "
This URL shortening services is powered by hedge.

    github.com/nerdypepper/hedge

To shorten urls:
    curl -F'shorten=https://shorten.some/long/url' {}
        ",
        String::from_utf8_lossy(host)
    );
    return Response::builder()
        .header("content-type", "text/plain")
        .body(Body::from(text))
        .unwrap();
}

fn respond_with_shortlink<S: AsRef<[u8]>>(shortlink: S, host: &[u8]) -> Response<Body> {
    let url = [host, b"/", shortlink.as_ref()].concat();
    info!("Successfully generated shortlink");
    Response::builder()
        .status(StatusCode::OK)
        .header("content-type", "text/html")
        .body(Body::from(url))
        .unwrap()
}

fn respond_with_status(s: StatusCode) -> Response<Body> {
    Response::builder().status(s).body(Body::empty()).unwrap()
}

fn shorten<S: AsRef<str>>(url: S, conn: &mut Connection) -> Result<String> {
    let mut stmt = conn.prepare("select * from urls where link = ?1")?;
    let mut rows = stmt.query(params![url.as_ref().to_string()])?;
    if let Some(row) = rows.next()? {
        return Ok(row.get(1)?);
    } else {
        let new_id = nanoid!(4);
        conn.execute(
            "insert into urls (link, shortlink) values (?1, ?2)",
            params![url.as_ref().to_string(), new_id],
        )?;
        return Ok(new_id);
    }
}

fn get_link<S: AsRef<str>>(url: S, conn: &Connection) -> Result<Option<String>> {
    let url = url.as_ref();
    let mut stmt = conn.prepare("select * from urls where shortlink = ?1")?;
    let mut rows = stmt.query(params![url.to_string()])?;
    if let Some(row) = rows.next()? {
        return Ok(row.get(0)?);
    } else {
        return Ok(None);
    }
}

async fn process_multipart(
    req: Request<Body>,
    boundary: String,
    conn: &mut Connection,
) -> Result<Response<Body>> {
    let _h = get_host(&req);
    let host = _h.as_ref().map(|h| h.as_bytes()).unwrap_or(b"");
    let mut m = Multipart::new(req.into_body(), boundary);
    if let Some(field) = m.next_field().await? {
        if field.name() == Some("shorten") {
            trace!("Recieved valid multipart request");
            let content = field
                .text()
                .await
                .with_context(|| format!("Expected field name"))?;

            let shortlink = shorten(content, conn)?;
            return Ok(respond_with_shortlink(shortlink, host));
        }
    }
    trace!("Unprocessable multipart request!");
    Ok(respond_with_status(StatusCode::UNPROCESSABLE_ENTITY))
}

async fn process_form(req: Request<Body>, conn: &mut Connection) -> Result<Response<Body>> {
    let _h = get_host(&req);
    let host = _h.as_ref().map(|h| h.as_bytes()).unwrap_or(b"");
    let b = hyper::body::to_bytes(req)
        .await
        .with_context(|| format!("Failed to stream request body!"))?;

    let params = form_urlencoded::parse(b.as_ref())
        .into_owned()
        .collect::<HashMap<String, String>>();

    if let Some(n) = params.get("shorten") {
        trace!("POST: {}", &n);
        let s = shorten(n, conn)?;
        return Ok(respond_with_shortlink(s, host));
    } else {
        error!("Invalid form");
        return Ok(respond_with_status(StatusCode::UNPROCESSABLE_ENTITY));
    }
}

pub async fn shortner_service(req: Request<Body>, mut conn: Connection) -> Result<Response<Body>> {
    match (req.method(), req.uri().path()) {
        (&Method::POST, "/") => {
            let boundary = req
                .headers()
                .get(CONTENT_TYPE)
                .and_then(|ct| ct.to_str().ok())
                .and_then(|ct| multer::parse_boundary(ct).ok());

            if boundary.is_none() {
                return process_form(req, &mut conn).await;
            }

            trace!("Attempting to parse multipart request");
            return process_multipart(req, boundary.unwrap(), &mut conn).await;
        }
        (&Method::GET, "/") => Ok(welcome(req)),
        (&Method::GET, _) => {
            trace!("GET: {}", req.uri());
            let shortlink = req.uri().path().to_string();
            let link = get_link(&shortlink[1..], &conn);
            if let Some(l) = link.unwrap() {
                trace!("Found in database, redirecting ...");
                Ok(Response::builder()
                    .header("Location", &l)
                    .header("content-type", "text/html")
                    .status(StatusCode::MOVED_PERMANENTLY)
                    .body(Body::from(format!(
                        "You will be redirected to: {}. If not, click the link.",
                        &l
                    )))?)
            } else {
                error!("Resource not found");
                Ok(respond_with_status(StatusCode::NOT_FOUND))
            }
        }
        _ => Ok(respond_with_status(StatusCode::NOT_FOUND)),
    }
}