|
| 1 | +## This module provides the ``Cookie`` type, which directly maps to Set-Cookie HTTP response headers, |
| 2 | +## and the ``CookieJar`` type which contains many cookies. |
| 3 | +## |
| 4 | +## Overview |
| 5 | +## ======================== |
| 6 | +## |
| 7 | +## ``Cookie`` type is used to generate Set-Cookie HTTP response headers. |
| 8 | +## Server sends Set-Cookie HTTP response headers to the user agent. |
| 9 | +## So the user agent can send them back to the server later. |
| 10 | +## |
| 11 | +## ``CookieJar`` contains many cookies from the user agent. |
| 12 | +## |
| 13 | + |
| 14 | + |
| 15 | +import options, times, strtabs, parseutils, strutils |
| 16 | + |
| 17 | + |
| 18 | +type |
| 19 | + SameSite* {.pure.} = enum ## The SameSite cookie attribute. |
| 20 | + None, Lax, Strict |
| 21 | + |
| 22 | + Cookie* = object ## Cookie type represents Set-Cookie HTTP response headers. |
| 23 | + name*, value*: string |
| 24 | + expires*: string |
| 25 | + maxAge*: Option[int] |
| 26 | + domain*: string |
| 27 | + path*: string |
| 28 | + secure*: bool |
| 29 | + httpOnly*: bool |
| 30 | + sameSite*: SameSite |
| 31 | + |
| 32 | + CookieJar* = object ## CookieJar type is a collection of cookies. |
| 33 | + data: StringTableRef |
| 34 | + |
| 35 | + MissingValueError* = object of ValueError ## Indicates an error associated with Cookie. |
| 36 | + |
| 37 | + |
| 38 | +proc initCookie*(name, value: string, expires = "", maxAge: Option[int] = none(int), |
| 39 | + domain = "", path = "", |
| 40 | + secure = false, httpOnly = false, sameSite = Lax): Cookie {.inline.} = |
| 41 | + ## Initiates Cookie object. |
| 42 | + runnableExamples: |
| 43 | + let |
| 44 | + username = "admin" |
| 45 | + message = "ok" |
| 46 | + cookie = initCookie(username, message) |
| 47 | + |
| 48 | + doAssert cookie.name == username |
| 49 | + doAssert cookie.value == message |
| 50 | + |
| 51 | + result = Cookie(name: name, value: value, expires: expires, |
| 52 | + maxAge: maxAge, domain: domain, path: path, |
| 53 | + secure: secure, httpOnly: httpOnly, sameSite: sameSite) |
| 54 | + |
| 55 | +proc initCookie*(name, value: string, expires: DateTime|Time, |
| 56 | + maxAge: Option[int] = none(int), domain = "", path = "", secure = false, httpOnly = false, |
| 57 | + sameSite = Lax): Cookie {.inline.} = |
| 58 | + ## Initiates Cookie object. |
| 59 | + runnableExamples: |
| 60 | + import times |
| 61 | + |
| 62 | + |
| 63 | + let |
| 64 | + username = "admin" |
| 65 | + message = "ok" |
| 66 | + expires = now() |
| 67 | + cookie = initCookie(username, message, expires) |
| 68 | + |
| 69 | + doAssert cookie.name == username |
| 70 | + doAssert cookie.value == message |
| 71 | + |
| 72 | + result = initCookie(name, value, format(expires.utc, |
| 73 | + "ddd',' dd MMM yyyy HH:mm:ss 'GMT'"), maxAge, domain, path, secure, |
| 74 | + httpOnly, sameSite) |
| 75 | + |
| 76 | +proc parseParams(cookie: var Cookie, key: string, value: string) {.inline.} = |
| 77 | + ## Parse Cookie attributes from key-value pairs. |
| 78 | + case key.toLowerAscii |
| 79 | + of "expires": |
| 80 | + if value.len != 0: |
| 81 | + cookie.expires = value |
| 82 | + of "maxage": |
| 83 | + try: |
| 84 | + cookie.maxAge = some(parseInt(value)) |
| 85 | + except ValueError: |
| 86 | + cookie.maxAge = none(int) |
| 87 | + of "domain": |
| 88 | + if value.len != 0: |
| 89 | + cookie.domain = value |
| 90 | + of "path": |
| 91 | + if value.len != 0: |
| 92 | + cookie.path = value |
| 93 | + of "secure": |
| 94 | + cookie.secure = true |
| 95 | + of "httponly": |
| 96 | + cookie.httpOnly = true |
| 97 | + of "samesite": |
| 98 | + case value.toLowerAscii |
| 99 | + of "none": |
| 100 | + cookie.sameSite = None |
| 101 | + of "strict": |
| 102 | + cookie.sameSite = Strict |
| 103 | + else: |
| 104 | + cookie.sameSite = Lax |
| 105 | + else: |
| 106 | + discard |
| 107 | + |
| 108 | +proc initCookie*(text: string): Cookie {.inline.} = |
| 109 | + ## Initiates Cookie object from strings. |
| 110 | + runnableExamples: |
| 111 | + doAssert initCookie("foo=bar=baz").name == "foo" |
| 112 | + doAssert initCookie("foo=bar=baz").value == "bar=baz" |
| 113 | + doAssert initCookie("foo=bar; HttpOnly").httpOnly |
| 114 | + |
| 115 | + var |
| 116 | + pos = 0 |
| 117 | + params: string |
| 118 | + name, value: string |
| 119 | + first = true |
| 120 | + |
| 121 | + while true: |
| 122 | + pos += skipWhile(text, {' ', '\t'}, pos) |
| 123 | + pos += parseUntil(text, params, ';', pos) |
| 124 | + |
| 125 | + var start = 0 |
| 126 | + start += parseUntil(params, name, '=', start) |
| 127 | + inc(start) # skip '=' |
| 128 | + if start < params.len: |
| 129 | + value = params[start .. ^1] |
| 130 | + else: |
| 131 | + value = "" |
| 132 | + |
| 133 | + if first: |
| 134 | + if name.len == 0: |
| 135 | + raise newException(MissingValueError, "cookie name is missing!") |
| 136 | + if value.len == 0: |
| 137 | + raise newException(MissingValueError, "cookie valie is missing!") |
| 138 | + result.name = name |
| 139 | + result.value = value |
| 140 | + first = false |
| 141 | + else: |
| 142 | + parseParams(result, name, value) |
| 143 | + if pos >= text.len: |
| 144 | + break |
| 145 | + inc(pos) # skip '; |
| 146 | + |
| 147 | +proc setCookie*(cookie: Cookie): string = |
| 148 | + ## Stringifys Cookie object to get Set-Cookie HTTP response headers. |
| 149 | + runnableExamples: |
| 150 | + import strformat |
| 151 | + |
| 152 | + |
| 153 | + let |
| 154 | + username = "admin" |
| 155 | + message = "ok" |
| 156 | + cookie = initCookie(username, message) |
| 157 | + |
| 158 | + doAssert setCookie(cookie) == fmt"{username}={message}; SameSite=Lax" |
| 159 | + |
| 160 | + result.add cookie.name & "=" & cookie.value |
| 161 | + if cookie.domain.strip.len != 0: |
| 162 | + result.add("; Domain=" & cookie.domain) |
| 163 | + if cookie.path.strip.len != 0: |
| 164 | + result.add("; Path=" & cookie.path) |
| 165 | + if cookie.maxAge.isSome: |
| 166 | + result.add("; Max-Age=" & $cookie.maxAge.get()) |
| 167 | + if cookie.expires.strip.len != 0: |
| 168 | + result.add("; Expires=" & cookie.expires) |
| 169 | + if cookie.secure: |
| 170 | + result.add("; Secure") |
| 171 | + if cookie.httpOnly: |
| 172 | + result.add("; HttpOnly") |
| 173 | + if cookie.sameSite != None: |
| 174 | + result.add("; SameSite=" & $cookie.sameSite) |
| 175 | + |
| 176 | +proc `$`*(cookie: Cookie): string {.inline.} = |
| 177 | + ## Stringifys Cookie object to get Set-Cookie HTTP response headers. |
| 178 | + runnableExamples: |
| 179 | + import strformat |
| 180 | + |
| 181 | + |
| 182 | + let |
| 183 | + username = "admin" |
| 184 | + message = "ok" |
| 185 | + cookie = initCookie(username, message) |
| 186 | + |
| 187 | + doAssert $cookie == fmt"{username}={message}; SameSite=Lax" |
| 188 | + |
| 189 | + setCookie(cookie) |
| 190 | + |
| 191 | +proc initCookieJar*(): CookieJar {.inline.} = |
| 192 | + ## Creates a new cookieJar that is empty. |
| 193 | + CookieJar(data: newStringTable(mode = modeCaseSensitive)) |
| 194 | + |
| 195 | +proc len*(cookieJar: CookieJar): int {.inline.} = |
| 196 | + ## Returns the number of names in ``cookieJar``. |
| 197 | + cookieJar.data.len |
| 198 | + |
| 199 | +proc `[]`*(cookieJar: CookieJar, name: string): string {.inline.} = |
| 200 | + ## Retrieves the value at ``cookieJar[name]``. |
| 201 | + ## |
| 202 | + ## If ``name`` is not in ``cookieJar``, the ``KeyError`` exception is raised. |
| 203 | + cookieJar.data[name] |
| 204 | + |
| 205 | +proc getOrDefault*(cookieJar: CookieJar, name: string, default = ""): string {.inline.} = |
| 206 | + ## Retrieves the value at ``cookieJar[name]`` if ``name`` is in ``cookieJar``. Otherwise, the |
| 207 | + ## default value is returned(default is ""). |
| 208 | + cookieJar.data.getOrDefault(name, default) |
| 209 | + |
| 210 | +proc hasKey*(cookieJar: CookieJar, name: string): bool {.inline.} = |
| 211 | + ## Returns true if ``name`` is in the ``cookieJar``. |
| 212 | + cookieJar.data.hasKey(name) |
| 213 | + |
| 214 | +proc contains*(cookieJar: CookieJar, name: string): bool {.inline.} = |
| 215 | + ## Returns true if ``name`` is in the ``cookieJar``. |
| 216 | + ## Alias of ``hasKey`` for use with the ``in`` operator. |
| 217 | + cookieJar.data.contains(name) |
| 218 | + |
| 219 | +proc `[]=`*(cookieJar: var CookieJar, name: string, value: string) {.inline.} = |
| 220 | + ## Inserts a ``(name, value)`` pair into ``cookieJar``. |
| 221 | + cookieJar.data[name] = value |
| 222 | + |
| 223 | +proc parse*(cookieJar: var CookieJar, text: string) {.inline.} = |
| 224 | + ## Parses CookieJar from strings. |
| 225 | + runnableExamples: |
| 226 | + var cookieJar = initCookieJar() |
| 227 | + cookieJar.parse("username=netkit; message=ok") |
| 228 | + |
| 229 | + doAssert cookieJar["username"] == "netkit" |
| 230 | + doAssert cookieJar["message"] == "ok" |
| 231 | + |
| 232 | + var |
| 233 | + pos = 0 |
| 234 | + name, value: string |
| 235 | + while true: |
| 236 | + pos += skipWhile(text, {' ', '\t'}, pos) |
| 237 | + pos += parseUntil(text, name, '=', pos) |
| 238 | + if pos >= text.len: |
| 239 | + break |
| 240 | + inc(pos) # skip '=' |
| 241 | + pos += parseUntil(text, value, ';', pos) |
| 242 | + cookieJar[name] = move(value) |
| 243 | + if pos >= text.len: |
| 244 | + break |
| 245 | + inc(pos) # skip ';' |
| 246 | + |
| 247 | +iterator pairs*(cookieJar: CookieJar): tuple[name, value: string] = |
| 248 | + ## Iterates over any ``(name, value)`` pair in the ``cookieJar``. |
| 249 | + for (name, value) in cookieJar.data.pairs: |
| 250 | + yield (name, value) |
| 251 | + |
| 252 | +iterator keys*(cookieJar: CookieJar): string = |
| 253 | + ## Iterates over any ``name`` in the ``cookieJar``. |
| 254 | + for name in cookieJar.data.keys: |
| 255 | + yield name |
| 256 | + |
| 257 | +iterator values*(cookieJar: CookieJar): string = |
| 258 | + ## Iterates over any ``value`` in the ``cookieJar``. |
| 259 | + for value in cookieJar.data.values: |
| 260 | + yield value |
| 261 | + |
| 262 | +proc secondsForward*(seconds: Natural): DateTime = |
| 263 | + ## Forward DateTime in seconds. |
| 264 | + getTime().utc + initDuration(seconds = seconds) |
| 265 | + |
| 266 | +proc daysForward*(days: Natural): DateTime = |
| 267 | + ## Forward DateTime in days. |
| 268 | + getTime().utc + initDuration(days = days) |
| 269 | + |
| 270 | +proc timesForward*(nanoseconds, microseconds, milliseconds, seconds, minutes, |
| 271 | + hours, days, weeks: Natural = 0): DateTime = |
| 272 | + ## Forward DateTime in seconds |
| 273 | + getTime().utc + initDuration(nanoseconds, microseconds, milliseconds, seconds, |
| 274 | + minutes, hours, days, weeks) |
0 commit comments