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
|
import options
import strutils
import times
import io/urlfilter
import js/javascript
import js/regex
import types/url
import utils/twtstr
type
Cookie* = ref object
name {.jsget.}: string
value {.jsget.}: string
expires {.jsget.}: int64 # unix time
maxAge {.jsget.}: int64
secure {.jsget.}: bool
httponly {.jsget.}: bool
samesite {.jsget.}: bool
domain {.jsget.}: string
path {.jsget.}: string
CookieJar* = ref object
filter*: URLFilter
cookies*: seq[Cookie]
proc parseCookieDate(val: string): Option[DateTime] =
# cookie-date
const Delimiters = {'\t', ' '..'/', ';'..'@', '['..'`', '{'..'~'}
const NonDigit = AllChars - AsciiDigit
var foundTime = false
var foundDayOfMonth = false
var foundMonth = false
var foundYear = false
# date-token-list
var time: array[3, int]
var dayOfMonth: int
var month: int
var year: int
for dateToken in val.split(Delimiters):
if dateToken == "": continue # *delimiter
if not foundTime:
block timeBlock: # test for time
let hmsTime = dateToken.until(NonDigit - {':'})
var i = 0
for timeField in hmsTime.split(':'):
if i > 2: break timeBlock # too many time fields
# 1*2DIGIT
if timeField.len != 1 and timeField.len != 2: break timeBlock
var timeFields: array[3, int]
for c in timeField:
if c notin AsciiDigit: break timeBlock
timeFields[i] *= 10
timeFields[i] += c.decValue
time = timeFields
inc i
if i != 3: break timeBlock
foundTime = true
continue
if not foundDayOfMonth:
block dayOfMonthBlock: # test for day-of-month
let digits = dateToken.until(NonDigit)
if digits.len != 1 and digits.len != 2: break dayOfMonthBlock
var n = 0
for c in digits:
if c notin AsciiDigit: break dayOfMonthBlock
n *= 10
n += c.decValue
dayOfMonth = n
foundDayOfMonth = true
continue
if not foundMonth:
block monthBlock: # test for month
if dateToken.len < 3: break monthBlock
case dateToken.substr(0, 2).toLower()
of "jan": month = 1
of "feb": month = 2
of "mar": month = 3
of "apr": month = 4
of "may": month = 5
of "jun": month = 6
of "jul": month = 7
of "aug": month = 8
of "sep": month = 9
of "oct": month = 10
of "nov": month = 11
of "dec": month = 12
else: break monthBlock
foundMonth = true
continue
if not foundYear:
block yearBlock: # test for year
let digits = dateToken.until(NonDigit)
if digits.len != 2 and digits.len != 4: break yearBlock
var n = 0
for c in digits:
if c notin AsciiDigit: break yearBlock
n *= 10
n += c.decValue
year = n
foundYear = true
continue
if not (foundDayOfMonth and foundMonth and foundYear and foundTime): return none(DateTime)
if dayOfMonth notin 0..31: return none(DateTime)
if year < 1601: return none(DateTime)
if time[0] > 23: return none(DateTime)
if time[1] > 59: return none(DateTime)
if time[2] > 59: return none(DateTime)
var dateTime = dateTime(year, Month(month), MonthdayRange(dayOfMonth), HourRange(time[0]), MinuteRange(time[1]), SecondRange(time[2]))
return some(dateTime)
# For debugging
proc `$`*(cookiejar: CookieJar): string =
result &= $cookiejar.filter
result &= "\n"
for cookie in cookiejar.cookies:
result &= "Cookie "
result &= $cookie[]
proc serialize*(cookiejar: CookieJar, location: URL): string =
if not cookiejar.filter.match(location):
return "" # fail
let t = now().toTime().toUnix()
for i in countdown(cookiejar.cookies.high, 0):
let cookie = cookiejar.cookies[i]
if cookie.expires != -1 and cookie.expires <= t:
cookiejar.cookies.delete(i)
elif cookie.domain == "" or location.host.endsWith(cookie.domain):
result.percentEncode(cookie.name, UserInfoPercentEncodeSet)
result &= "="
result.percentEncode(cookie.value, UserInfoPercentEncodeSet)
result &= ";"
proc newCookie*(str: string): Cookie {.jsctor.} =
let cookie = new(Cookie)
cookie.expires = -1
var first = true
for part in str.split(';'):
if first:
cookie.name = part.until('=')
cookie.value = part.after('=')
first = false
continue
let part = percentDecode(part).strip(leading = true, trailing = false, AsciiWhitespace)
var n = 0
for i in 0..part.high:
if part[i] == '=':
n = i
break
if n == 0:
continue
let key = part.substr(0, n - 1)
let val = part.substr(n + 1)
case key.toLower()
of "expires":
let date = parseCookieDate(val)
if date.issome:
cookie.expires = date.get.toTime().toUnix()
of "max-age":
cookie.expires = now().toTime().toUnix() + parseInt64(val)
of "secure": cookie.secure = true
of "httponly": cookie.httponly = true
of "samesite": cookie.samesite = true
of "path": cookie.path = val
of "domain": cookie.domain = val
return cookie
proc newCookieJar*(location: URL, allowhosts: seq[Regex]): CookieJar =
return CookieJar(
filter: newURLFilter(
scheme = some(location.scheme),
allowhost = some(location.host),
allowhosts = allowhosts
)
)
proc addCookieModule*(ctx: JSContext) =
ctx.registerType(Cookie)
|