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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
|
# Copyright (C) 2012 Dominik Picheta
# MIT License - Look at license.txt for details.
import parseutils, strtabs, strutils, tables, net, mimetypes, asyncdispatch, os
from cgi import decodeUrl
const
useHttpBeast* = false # not defined(windows) and not defined(useStdLib)
type
MultiData* = OrderedTable[string, tuple[fields: StringTableRef, body: string]]
Settings* = ref object
staticDir*: string # By default ./public
appName*: string
mimes*: MimeDb
port*: Port
bindAddr*: string
reusePort*: bool
futureErrorHandler*: proc (fut: Future[void]) {.closure, gcsafe.}
JesterError* = object of Exception
proc parseUrlQuery*(query: string, result: var Table[string, string])
{.deprecated: "use stdlib".} =
var i = 0
i = query.skip("?")
while i < query.len()-1:
var key = ""
var val = ""
i += query.parseUntil(key, '=', i)
if query[i] != '=':
raise newException(ValueError, "Expected '=' at " & $i &
" but got: " & $query[i])
inc(i) # Skip =
i += query.parseUntil(val, '&', i)
inc(i) # Skip &
result[decodeUrl(key)] = decodeUrl(val)
template parseContentDisposition(): typed =
var hCount = 0
while hCount < hValue.len()-1:
var key = ""
hCount += hValue.parseUntil(key, {';', '='}, hCount)
if hValue[hCount] == '=':
var value = hvalue.captureBetween('"', start = hCount)
hCount += value.len+2
inc(hCount) # Skip ;
hCount += hValue.skipWhitespace(hCount)
if key == "name": name = value
newPart[0][key] = value
else:
inc(hCount)
hCount += hValue.skipWhitespace(hCount)
proc parseMultiPart*(body: string, boundary: string): MultiData =
result = initOrderedTable[string, tuple[fields: StringTableRef, body: string]]()
var mboundary = "--" & boundary
var i = 0
var partsLeft = true
while partsLeft:
var firstBoundary = body.skip(mboundary, i)
if firstBoundary == 0:
raise newException(ValueError, "Expected boundary. Got: " & body.substr(i, i+25))
i += firstBoundary
i += body.skipWhitespace(i)
# Headers
var newPart: tuple[fields: StringTableRef, body: string] = ({:}.newStringTable, "")
var name = ""
while true:
if body[i] == '\c':
inc(i, 2) # Skip \c\L
break
var hName = ""
i += body.parseUntil(hName, ':', i)
if body[i] != ':':
raise newException(ValueError, "Expected : in headers.")
inc(i) # Skip :
i += body.skipWhitespace(i)
var hValue = ""
i += body.parseUntil(hValue, {'\c', '\L'}, i)
if toLowerAscii(hName) == "content-disposition":
parseContentDisposition()
newPart[0][hName] = hValue
i += body.skip("\c\L", i) # Skip *one* \c\L
# Parse body.
while true:
if body[i] == '\c' and body[i+1] == '\L' and
body.skip(mboundary, i+2) != 0:
if body.skip("--", i+2+mboundary.len) != 0:
partsLeft = false
break
break
else:
newPart[1].add(body[i])
inc(i)
i += body.skipWhitespace(i)
result.add(name, newPart)
proc parseMPFD*(contentType: string, body: string): MultiData =
var boundaryEqIndex = contentType.find("boundary=")+9
var boundary = contentType.substr(boundaryEqIndex, contentType.len()-1)
return parseMultiPart(body, boundary)
proc parseCookies*(s: string): Table[string, string] =
## parses cookies into a string table.
##
## The proc is meant to parse the Cookie header set by a client, not the
## "Set-Cookie" header set by servers.
result = initTable[string, string]()
var i = 0
while true:
i += skipWhile(s, {' ', '\t'}, i)
var keystart = i
i += skipUntil(s, {'='}, i)
var keyend = i-1
if i >= len(s): break
inc(i) # skip '='
var valstart = i
i += skipUntil(s, {';'}, i)
result[substr(s, keystart, keyend)] = substr(s, valstart, i-1)
if i >= len(s): break
inc(i) # skip ';'
type
SameSite* = enum
None, Lax, Strict
proc makeCookie*(key, value, expires: string, domain = "", path = "",
secure = false, httpOnly = false,
sameSite = Lax): string =
result = ""
result.add key & "=" & value
if domain != "": result.add("; Domain=" & domain)
if path != "": result.add("; Path=" & path)
if expires != "": result.add("; Expires=" & expires)
if secure: result.add("; Secure")
if httpOnly: result.add("; HttpOnly")
if sameSite != None:
result.add("; SameSite=" & $sameSite)
when not declared(tables.getOrDefault):
template getOrDefault*(tab, key): untyped = tab[key]
when not declared(normalizePath) and not declared(normalizedPath):
proc normalizePath*(path: var string) =
## Normalize a path.
##
## Consecutive directory separators are collapsed, including an initial double slash.
##
## On relative paths, double dot (..) sequences are collapsed if possible.
## On absolute paths they are always collapsed.
##
## Warning: URL-encoded and Unicode attempts at directory traversal are not detected.
## Triple dot is not handled.
let isAbs = isAbsolute(path)
var stack: seq[string] = @[]
for p in split(path, {DirSep}):
case p
of "", ".":
continue
of "..":
if stack.len == 0:
if isAbs:
discard # collapse all double dots on absoluta paths
else:
stack.add(p)
elif stack[^1] == "..":
stack.add(p)
else:
discard stack.pop()
else:
stack.add(p)
if isAbs:
path = DirSep & join(stack, $DirSep)
elif stack.len > 0:
path = join(stack, $DirSep)
else:
path = "."
proc normalizedPath*(path: string): string =
## Returns a normalized path for the current OS. See `<#normalizePath>`_
result = path
normalizePath(result)
when isMainModule:
var r = {:}.newStringTable
parseUrlQuery("FirstName=Mickey", r)
echo r
|