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
|
import os, strutils
import std/[hashes, re, sequtils, times, uri]
import tiny_sqlite
import jester
import nimja/parser
import nanoid
import database
const nanoidAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
const nanoidSize = 10
const allCss = staticRead("../static/all.css")
const cssRoute = "/static/all.css." & $hash(allCss)
const favicon = staticRead("../static/favicon.ico")
const faviconSvg = staticRead("../static/favicon.svg")
const secureHeaders = @[
("X-Frame-Options", "deny"),
("X-XSS-Protection", "1; mode=block"),
("X-Content-Type-Options", "nosniff"),
("Referrer-Policy", "strict-origin"),
("Cache-Control", "no-transform"),
("Content-Security-Policy", "script-src 'self'"),
("Permissions-Policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"),
("Strict-Transport-Security", "max-age=16000000;"),
]
const nonCachingHeaders = concat(secureHeaders, @[("Cache-Control", "max-age=0" )])
const htmlHeaders = concat(nonCachingHeaders, @[("content-type", "text/html")])
const cachingHeaders = concat(secureHeaders, @[("Cache-Control", "public, max-age=31536000, immutable" )])
const cssHeaders = concat(cachingHeaders, @[("content-type", "text/css")])
const icoHeaders = concat(cachingHeaders, @[("content-type", "image/x-icon")])
const svgHeaders = concat(cachingHeaders, @[("content-type", "image/svg+xml")])
var db {.threadvar.}: DbConn
proc initDB() {.raises: [SqliteError].} =
if not db.isOpen():
db = openDatabase("data/short.db")
if not db.Migrate():
echo "Failed to migrate database schema"
quit 1
func renderIndex(): string {.raises: [].} =
var req: ShortUrl
compileTemplateFile(getScriptDir() / "templates/index.html")
func renderAbout(): string {.raises: [].} =
compileTemplateFile(getScriptDir() / "templates/about.html")
func renderShort(req: ShortUrl): string {.raises: [].} =
compileTemplateFile(getScriptDir() / "templates/short.html")
func renderNoShort(req: ShortUrl): string {.raises: [].} =
compileTemplateFile(getScriptDir() / "templates/noshort.html")
func renderError(code: int, msg: string): string {.raises: [].} =
compileTemplateFile(getScriptDir() / "templates/error.html")
proc handleToken(token: string): (HttpCode, string) {.raises: [].} =
try:
let tokenRegexp = re"^[\w]{10}$"
if not match(token, tokenRegexp):
return (Http400, renderError(400, "Bad Request"))
except RegexError:
return (Http500, renderError(500, "RegexError"))
db.CleanExpired()
try:
let req = db.GetUrl(token)
if req == nil:
return (Http404, renderNoShort(req[]))
return (Http200, renderShort(req[]))
except SqliteError:
return (Http500, renderError(500, "SqliteError"))
proc handleIndexPost(params: Table[string, string]): (HttpCode, string) {.raises: [].} =
var input: ShortUrl
var exp: int
for k, v in params.pairs:
case k:
of "title":
try:
let titleRegexp = re"^[\w\s]{3,64}$"
if match(v, titleRegexp):
input.Title = v
else:
return (Http400, renderError(400, "Bad Request"))
except RegexError:
return (Http500, renderError(500, "RegexError"))
of "url":
try:
discard parseUri(v)
input.Url = v
except:
return (Http400, renderError(400, "Bad Request"))
of "expires":
try:
exp = parseInt(v)
except ValueError:
return (Http400, renderError(400, "Bad Request"))
if exp < 1 or exp > 527040:
return (Http400, renderError(400, "Bad Request"))
of "shorten": discard
else: return (Http400, renderError(400, "Bad Request"))
if input.Title == "" or input.Url == "" or exp == 0:
return (Http400, renderError(400, "Bad Request"))
try:
input.Token = generate(alphabet=nanoidAlphabet, size=nanoidSize)
except IOError:
return (Http500, renderError(500, "IOError on UUID generation"))
except OSError:
return (Http500, renderError(500, "OSError on UUID generation"))
input.Created = times.now()
input.Expires = input.Created + initDuration(minutes = exp)
try:
db.AddUrl(input)
except SqliteError:
return (Http500, renderShort(input))
return (Http200, $input.Token)
routes:
get "/":
resp Http200, htmlHeaders, renderIndex()
get "/about":
resp Http200, htmlHeaders, renderAbout()
post "/":
initDB()
var (code, content) = handleIndexPost(request.params)
if code != Http200:
resp code, htmlHeaders, content
else:
redirect("/" & content)
get "/static/favicon.ico":
resp Http200, icoHeaders, favicon
get "/static/favicon.svg":
resp Http200, svgHeaders, faviconSvg
get re"^/static/all\.css\.":
resp Http200, cssHeaders, allcss
get "/@token":
initDB()
var (code, content) = handleToken(@"token")
resp code, htmlHeaders, content
when isMainModule:
runForever()
|