diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/config/config.nim | 25 | ||||
-rw-r--r-- | src/config/cookie.nim | 435 | ||||
-rw-r--r-- | src/local/container.nim | 5 | ||||
-rw-r--r-- | src/local/pager.nim | 57 | ||||
-rw-r--r-- | src/server/buffer.nim | 1 | ||||
-rw-r--r-- | src/server/loader.nim | 2 | ||||
-rw-r--r-- | src/server/loaderiface.nim | 2 | ||||
-rw-r--r-- | src/types/cookie.nim | 253 | ||||
-rw-r--r-- | src/types/url.nim | 4 |
9 files changed, 501 insertions, 283 deletions
diff --git a/src/config/config.nim b/src/config/config.nim index 690014b5..734a3a04 100644 --- a/src/config/config.nim +++ b/src/config/config.nim @@ -5,6 +5,7 @@ import std/tables import chagashi/charset import config/chapath +import config/cookie import config/mailcap import config/mimetypes import config/toml @@ -21,7 +22,6 @@ import monoucha/tojs import server/headers import types/cell import types/color -import types/cookie import types/jscolor import types/opt import types/url @@ -56,11 +56,16 @@ type frtData = "data" frtMailto = "mailto" + CookieMode* = enum + cmNone = "false" + cmReadOnly = "true" + cmSave = "save" + SiteConfig* = ref object url*: Option[Regex] host*: Option[Regex] rewrite_url*: Option[JSValueFunction] - cookie*: Option[bool] + cookie*: Option[CookieMode] share_cookie_jar*: Option[string] referer_from*: Option[bool] scripting*: Option[ScriptingMode] @@ -113,6 +118,7 @@ type bookmark* {.jsgetset.}: ChaPathResolved history_file*: ChaPathResolved history_size* {.jsgetset.}: int32 + cookie_file*: ChaPathResolved download_dir* {.jsgetset.}: ChaPathResolved w3m_cgi_compat* {.jsgetset.}: bool copy_cmd* {.jsgetset.}: string @@ -161,7 +167,7 @@ type styling* {.jsgetset.}: bool scripting* {.jsgetset.}: ScriptingMode images* {.jsgetset.}: bool - cookie* {.jsgetset.}: bool + cookie* {.jsgetset.}: CookieMode referer_from* {.jsgetset.}: bool autofocus* {.jsgetset.}: bool meta_refresh* {.jsgetset.}: MetaRefresh @@ -333,6 +339,8 @@ proc parseConfigValue(ctx: var ConfigParser; x: var ColorMode; v: TomlValue; k: string) proc parseConfigValue(ctx: var ConfigParser; x: var ScriptingMode; v: TomlValue; k: string) +proc parseConfigValue(ctx: var ConfigParser; x: var CookieMode; v: TomlValue; + k: string) proc parseConfigValue[T](ctx: var ConfigParser; x: var Option[T]; v: TomlValue; k: string) proc parseConfigValue(ctx: var ConfigParser; x: var ARGBColor; v: TomlValue; @@ -497,6 +505,17 @@ proc parseConfigValue(ctx: var ConfigParser; x: var ScriptingMode; v: TomlValue; raise newException(ValueError, "unknown scripting mode '" & v.s & "' for key " & k) +proc parseConfigValue(ctx: var ConfigParser; x: var CookieMode; v: TomlValue; + k: string) = + typeCheck(v, {tvtString, tvtBoolean}, k) + if v.t == tvtBoolean: + x = if v.b: cmReadOnly else: cmNone + elif v.s == "save": + x = cmSave + else: + raise newException(ValueError, "unknown cookie mode '" & v.s & + "' for key " & k) + proc parseConfigValue(ctx: var ConfigParser; x: var ARGBColor; v: TomlValue; k: string) = typeCheck(v, tvtString, k) diff --git a/src/config/cookie.nim b/src/config/cookie.nim new file mode 100644 index 00000000..219f35fe --- /dev/null +++ b/src/config/cookie.nim @@ -0,0 +1,435 @@ +import std/algorithm +import std/options +import std/posix +import std/strutils +import std/tables +import std/times + +import io/dynstream +import types/opt +import types/url +import utils/twtstr + +type + Cookie* = ref object + name: string + value: string + expires: int64 # unix time + domain: string + path: string + persist: bool + secure: bool + httpOnly: bool + hostOnly: bool + isnew: bool + skip: bool + + CookieJar* = ref object + cookies*: seq[Cookie] + map: Table[string, Cookie] # {host}{path}\t{name} + + CookieJarMap* = ref object + mtime: int64 + jars*: OrderedTable[string, CookieJar] + +proc newCookieJarMap*(): CookieJarMap = + return CookieJarMap() + +proc newCookieJar*(): CookieJar = + return CookieJar() + +proc parseCookieDate(val: string): Option[int64] = + # 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].default + var dayOfMonth = 0 + var month = 0 + var year = 0 + 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].default + 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).toLowerAscii() + 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(int64) + if dayOfMonth notin 0..31: return none(int64) + if year < 1601: return none(int64) + if time[0] > 23: return none(int64) + if time[1] > 59: return none(int64) + if time[2] > 59: return none(int64) + let dt = dateTime(year, Month(month), MonthdayRange(dayOfMonth), + HourRange(time[0]), MinuteRange(time[1]), SecondRange(time[2])) + return some(dt.toTime().toUnix()) + +# For debugging +proc `$`*(cookieJar: CookieJar): string = + result = "" + for cookie in cookieJar.cookies: + result &= "Cookie " + result &= $cookie[] + result &= "\n" + +# https://www.rfc-editor.org/rfc/rfc6265#section-5.1.4 +func defaultCookiePath(url: URL): string = + var path = url.pathname.untilLast('/') + if path == "" or path[0] != '/': + return "/" + return move(path) + +func cookiePathMatches(cookiePath, requestPath: string): bool = + if requestPath.startsWith(cookiePath): + if requestPath.len == cookiePath.len: + return true + if cookiePath[^1] == '/': + return true + if requestPath.len > cookiePath.len and requestPath[cookiePath.len] == '/': + return true + return false + +func cookieDomainMatches(cookieDomain: string; url: URL): bool = + if cookieDomain.len == 0: + return false + let host = url.host + if url.isIP(): + return host == cookieDomain + if host.endsWith(cookieDomain) and host.len >= cookieDomain.len: + return host.len == cookieDomain.len or + host[host.len - cookieDomain.len - 1] == '.' + return false + +proc add(cookieJar: CookieJar; cookie: Cookie; parseMode = false, + persist = true) = + let s = cookie.domain & cookie.path & '\t' & cookie.name + let old = cookieJar.map.getOrDefault(s) + if old != nil: + if parseMode and old.isnew: + return # do not override newly added cookies + if persist or not old.persist: + let i = cookieJar.cookies.find(old) + cookieJar.cookies.delete(i) + else: + # we cannot save this cookie, but it must be kept for this session. + old.skip = true + cookieJar.map[s] = cookie + cookieJar.cookies.add(cookie) + +# https://www.rfc-editor.org/rfc/rfc6265#section-5.4 +proc serialize*(cookieJar: CookieJar; url: URL): string = + var res = "" + let t = getTime().toUnix() + var expired: seq[int] = @[] + for i, cookie in cookieJar.cookies.mypairs: + let cookie = cookieJar.cookies[i] + if cookie.skip: # "read-only" cookie + continue + if cookie.expires != -1 and cookie.expires <= t: + expired.add(i) + continue + if cookie.secure and url.scheme != "https": + continue + if not cookiePathMatches(cookie.path, url.pathname): + continue + if cookie.hostOnly and cookie.domain != url.host: + continue + if not cookie.hostOnly and not cookieDomainMatches(cookie.domain, url): + continue + if res != "": + res &= "; " + res &= cookie.name + res &= "=" + res &= cookie.value + for j in countdown(expired.high, 0): + cookieJar.cookies.delete(expired[j]) + return move(res) + +proc parseSetCookie(str: string; t: int64; url: URL; persist: bool): + Opt[Cookie] = + let cookie = Cookie( + expires: -1, + hostOnly: true, + persist: persist, + isnew: true + ) + var first = true + var hasPath = false + for part in str.split(';'): + if first: + if '\t' in part: + # Drop cookie if it has a tab. + # Gecko seems to accept it, but Blink drops it too, + # so this should be safe from a compat perspective. + continue + cookie.name = part.until('=') + cookie.value = part.substr(cookie.name.len + 1) + first = false + continue + let part = part.strip(leading = true, trailing = false, AsciiWhitespace) + let key = part.untilLower('=') + let val = part.substr(key.len + 1) + case key + of "expires": + if cookie.expires == -1: + let date = parseCookieDate(val) + if date.isSome: + cookie.expires = date.get + of "max-age": + let x = parseInt32(val) + if x.get(-1) >= 0: + cookie.expires = t + x.get + of "secure": cookie.secure = true + of "httponly": cookie.httpOnly = true + of "path": + if val != "" and val[0] == '/' and '\t' notin val: + hasPath = true + cookie.path = val + of "domain": + var hostType = htNone + var domain = parseHost(val, special = false, hostType) + if domain.len > 0 and domain[0] == '.': + domain.delete(0..0) + if hostType == htNone or not cookieDomainMatches(domain, url): + return err() + if hostType != htNone: + cookie.domain = move(domain) + cookie.hostOnly = false + if cookie.hostOnly: + cookie.domain = url.host + if not hasPath: + cookie.path = defaultCookiePath(url) + if cookie.expires < 0: + cookie.persist = false + return ok(cookie) + +proc setCookie*(cookieJar: CookieJar; header: openArray[string]; url: URL; + persist: bool) = + let t = getTime().toUnix() + var sorted = true + for s in header: + let cookie = parseSetCookie(s, t, url, persist) + if cookie.isSome: + cookieJar.add(cookie.get, persist = persist) + sorted = false + if not sorted: + cookieJar.cookies.sort(proc(a, b: Cookie): int = + return cmp(a.path.len, b.path.len), order = Descending) + +type ParseState = object + i: int + cookie: Cookie + error: bool + +proc nextField(state: var ParseState; iq: openArray[char]): string = + if state.i >= iq.len or iq[state.i] == '\n': + state.error = true + return "" + var field = iq.until({'\t', '\n'}, state.i) + state.i += field.len + if state.i < iq.len and iq[state.i] == '\t': + inc state.i + return move(field) + +proc nextBool(state: var ParseState; iq: openArray[char]): bool = + let field = state.nextField(iq) + if field == "TRUE": + return true + if field != "FALSE": + state.error = true + return false + +proc nextInt64(state: var ParseState; iq: openArray[char]): int64 = + let x = parseInt64(state.nextField(iq)) + if x.isNone: + state.error = true + return 0 + return x.get + +proc parse(map: CookieJarMap; iq: openArray[char]; warnings: var seq[string]) = + var state = ParseState() + var line = 0 + while state.i < iq.len: + var httpOnly = false + if iq[state.i] == '\n': + inc state.i + continue + if iq[state.i] == '#': + inc state.i + let first = iq.until({'_', '\n'}, state.i) + state.i += first.len + if first != "HttpOnly": + while state.i < iq.len and iq[state.i] != '\n': + inc state.i + inc state.i + inc line + continue + inc state.i + httpOnly = true + state.error = false + let cookie = Cookie(httpOnly: httpOnly, persist: true) + var domain = state.nextField(iq) + var cookieJar: CookieJar = nil + if (let j = domain.find('@'); j != -1): + cookie.domain = domain.substr(j + 1) + if cookie.domain[0] == '.': + cookie.domain.delete(0..0) + domain.setLen(j) + else: + if domain[0] == '.': + domain.delete(0..0) + cookie.domain = domain + cookieJar = map.jars.getOrDefault(domain) + if cookieJar == nil: + cookieJar = CookieJar() + map.jars[domain] = cookieJar + cookie.hostOnly = not state.nextBool(iq) + cookie.path = state.nextField(iq) + cookie.secure = state.nextBool(iq) + cookie.expires = state.nextInt64(iq) + cookie.name = state.nextField(iq) + cookie.value = state.nextField(iq) + if not state.error: + cookieJar.add(cookie, parseMode = true) + else: + warnings.add("skipped invalid cookie line " & $line) + inc state.i + inc line + +# Consumes `ps'. +proc parse*(map: CookieJarMap; ps: PosixStream; mtime: int64; + warnings: var seq[string]) = + try: + let src = ps.recvAllOrMmap() + map.parse(src.toOpenArray(), warnings) + deallocMem(src) + map.mtime = mtime + except IOError: + discard + finally: + ps.sclose() + +proc c_rename(oldname, newname: cstring): cint {.importc: "rename", + header: "<stdio.h>".} + +proc write*(map: CookieJarMap; file: string): bool = + let ps = newPosixStream(file) + if ps != nil: + var stats: Stat + if fstat(ps.fd, stats) != -1 and S_ISREG(stats.st_mode): + if int64(stats.st_mtime) > map.mtime: + var dummy: seq[string] = @[] + map.parse(ps, int64(stats.st_mtime), dummy) + else: + ps.sclose() + else: + ps.sclose() + elif map.jars.len == 0: + return true + let tmp = file & '~' + block write: + let ps = newPosixStream(tmp, O_WRONLY or O_CREAT, 0o600) + var i = 0 + let time = getTime().toUnix() + try: + if ps != nil: + var buf = """ +# Netscape HTTP Cookie file +# Autogenerated by Chawan. Manually added cookies are normally +# preserved, but comments will be lost. + +""" + for name, jar in map.jars: + for cookie in jar.cookies: + if cookie.expires <= time or not cookie.persist: + continue # session cookie + if buf.len >= 4096: # flush + ps.sendDataLoop(buf) + buf.setLen(0) + if cookie.httpOnly: + buf &= "#HttpOnly_" + if cookie.domain != name: + buf &= name & "@" + if not cookie.hostOnly: + buf &= '.' + buf &= cookie.domain & '\t' + const BoolMap = [false: "FALSE", true: "TRUE"] + buf &= BoolMap[not cookie.hostOnly] & '\t' # flipped intentionally + buf &= cookie.path & '\t' + buf &= BoolMap[cookie.secure] & '\t' + buf &= $cookie.expires & '\t' + buf &= cookie.name & '\t' + buf &= cookie.value & '\n' + inc i + if buf.len > 0: + ps.sendDataLoop(buf) + except IOError: + return false + finally: + ps.sclose() + if i == 0: + discard unlink(cstring(tmp)) + discard unlink(cstring(file)) + return true + return c_rename(cstring(tmp), file) == 0 diff --git a/src/local/container.nim b/src/local/container.nim index 36d6f91d..a64fade0 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -6,6 +6,7 @@ import std/tables import chagashi/charset import config/config +import config/cookie import config/mimetypes import css/render import io/dynstream @@ -23,7 +24,6 @@ import types/bitmap import types/blob import types/cell import types/color -import types/cookie import types/opt import types/referrer import types/url @@ -1518,7 +1518,8 @@ proc applyResponse*(container: Container; response: Response; # accept cookies let cookieJar = container.loaderConfig.cookieJar if cookieJar != nil and "Set-Cookie" in response.headers.table: - cookieJar.setCookie(response.headers.table["Set-Cookie"], response.url) + cookieJar.setCookie(response.headers.table["Set-Cookie"], response.url, + container.config.cookieMode == cmSave) # set referrer policy, if any let referrerPolicy = response.getReferrerPolicy() if container.config.refererFrom: diff --git a/src/local/pager.nim b/src/local/pager.nim index 91fc45c4..3ad46381 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -13,6 +13,7 @@ import chagashi/charset import chagashi/decoder import config/chapath import config/config +import config/cookie import config/history import config/mailcap import config/mimetypes @@ -50,7 +51,6 @@ import types/bitmap import types/blob import types/cell import types/color -import types/cookie import types/opt import types/url import types/winattrs @@ -144,7 +144,7 @@ type config*: Config consoleWrapper*: ConsoleWrapper container {.jsget: "buffer".}: Container - cookiejars: Table[string, CookieJar] + cookieJars: CookieJarMap display: Surface exitCode*: int feednext*: bool @@ -476,7 +476,8 @@ proc newPager*(config: Config; forkserver: ForkServer; ctx: JSContext; luctx: LUContext(), urandom: urandom, exitCode: -1, - loader: loader + loader: loader, + cookieJars: newCookieJarMap() ) pager.timeouts = newTimeoutState(pager.jsctx, evalJSFree, pager) JS_SetModuleLoaderFunc(pager.jsrt, normalizeModuleName, clientLoadJSModule, @@ -487,15 +488,24 @@ proc newPager*(config: Config; forkserver: ForkServer; ctx: JSContext; proxy: pager.config.network.proxy, filter: newURLFilter(default = true), )) - let hist = newHistory(pager.config.external.history_size, getTime().toUnix()) - let ps = newPosixStream(pager.config.external.history_file) - if ps != nil: - var stat: Stat - if fstat(ps.fd, stat) != -1: - discard hist.parse(ps, int64(stat.st_mtime)) - else: - ps.sclose() - pager.lineHist[lmLocation] = hist + block history: + let hist = newHistory(pager.config.external.history_size, getTime().toUnix()) + let ps = newPosixStream(pager.config.external.history_file) + if ps != nil: + var stat: Stat + if fstat(ps.fd, stat) != -1: + discard hist.parse(ps, int64(stat.st_mtime)) + else: + ps.sclose() + pager.lineHist[lmLocation] = hist + block cookie: + let ps = newPosixStream(pager.config.external.cookie_file) + if ps != nil: + var stat: Stat + if fstat(ps.fd, stat) != -1: + pager.cookieJars.parse(ps, int64(stat.st_mtime), pager.alerts) + else: + ps.sclose() return pager proc cleanup(pager: Pager) = @@ -505,6 +515,8 @@ proc cleanup(pager: Pager) = let hist = pager.lineHist[lmLocation] if not hist.write(pager.config.external.history_file): pager.alert("failed to save history") + if not pager.cookieJars.write(pager.config.external.cookie_file): + pager.alert("failed to save cookies") for msg in pager.alerts: stderr.write("cha: " & msg & '\n') for val in pager.config.cmd.map.values: @@ -1805,7 +1817,8 @@ proc applySiteconf(pager: Pager; url: URL; charsetOverride: Charset; isdump: pager.config.start.headless, charsetOverride: charsetOverride, protocol: pager.config.protocol, - metaRefresh: pager.config.buffer.meta_refresh + metaRefresh: pager.config.buffer.meta_refresh, + cookieMode: pager.config.buffer.cookie ) loaderConfig = LoaderClientConfig( defaultHeaders: newHeaders(pager.config.network.default_headers), @@ -1818,6 +1831,7 @@ proc applySiteconf(pager: Pager; url: URL; charsetOverride: Charset; ), insecureSSLNoVerify: false ) + var cookieJarId = url.host let surl = $url for sc in pager.config.siteconf: if sc.url.isSome and not sc.url.get.match(surl): @@ -1845,14 +1859,9 @@ proc applySiteconf(pager: Pager; url: URL; charsetOverride: Charset; ourl = tmpUrl return if sc.cookie.isSome: - if sc.cookie.get: - # host/url might have changed by now - let jarid = sc.share_cookie_jar.get(url.host) - if jarid notin pager.cookiejars: - pager.cookiejars[jarid] = newCookieJar(url) - loaderConfig.cookieJar = pager.cookiejars[jarid] - else: - loaderConfig.cookieJar = nil # override + res.cookieMode = sc.cookie.get + if sc.share_cookie_jar.isSome: + cookieJarId = sc.share_cookie_jar.get if sc.scripting.isSome: res.scripting = sc.scripting.get if sc.referer_from.isSome: @@ -1881,6 +1890,12 @@ proc applySiteconf(pager: Pager; url: URL; charsetOverride: Charset; if res.images: res.imageTypes = pager.config.external.mime_types.image res.userAgent = loaderConfig.defaultHeaders.getOrDefault("User-Agent") + if res.cookieMode != cmNone: + var cookieJar = pager.cookieJars.jars.getOrDefault(cookieJarId) + if cookieJar == nil: + cookieJar = newCookieJar() + pager.cookieJars.jars[cookieJarId] = cookieJar + loaderConfig.cookieJar = cookieJar return res # Load request in a new buffer. diff --git a/src/server/buffer.nim b/src/server/buffer.nim index ea6a0d94..017b0965 100644 --- a/src/server/buffer.nim +++ b/src/server/buffer.nim @@ -138,6 +138,7 @@ type protocol*: Table[string, ProtocolConfig] imageTypes*: Table[string, string] userAgent*: string + cookieMode*: CookieMode GetValueProc = proc(iface: BufferInterface; promise: EmptyPromise) {.nimcall.} diff --git a/src/server/loader.nim b/src/server/loader.nim index 93ecc1bb..b5e0ee91 100644 --- a/src/server/loader.nim +++ b/src/server/loader.nim @@ -27,6 +27,7 @@ import std/posix import std/strutils import std/tables +import config/cookie import config/urimethodmap import io/bufreader import io/bufwriter @@ -39,7 +40,6 @@ import server/headers import server/loaderiface import server/request import server/urlfilter -import types/cookie import types/formdata import types/opt import types/referrer diff --git a/src/server/loaderiface.nim b/src/server/loaderiface.nim index faa9b021..d76ee14f 100644 --- a/src/server/loaderiface.nim +++ b/src/server/loaderiface.nim @@ -5,6 +5,7 @@ import std/tables +import config/cookie import io/bufreader import io/bufwriter import io/dynstream @@ -15,7 +16,6 @@ import server/headers import server/request import server/response import server/urlfilter -import types/cookie import types/opt import types/referrer import types/url diff --git a/src/types/cookie.nim b/src/types/cookie.nim deleted file mode 100644 index ee287004..00000000 --- a/src/types/cookie.nim +++ /dev/null @@ -1,253 +0,0 @@ -import std/options -import std/strutils -import std/times - -import types/opt -import types/url -import utils/twtstr - -type - Cookie* = ref object - name: string - value: string - expires: int64 # unix time - secure: bool - httponly: bool - hostOnly: bool - domain: string - path: string - - CookieJar* = ref object - domain: string - cookies*: seq[Cookie] - -proc parseCookieDate(val: string): Option[int64] = - # 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].default - var dayOfMonth = 0 - var month = 0 - var year = 0 - 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].default - 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).toLowerAscii() - 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(int64) - if dayOfMonth notin 0..31: return none(int64) - if year < 1601: return none(int64) - if time[0] > 23: return none(int64) - if time[1] > 59: return none(int64) - if time[2] > 59: return none(int64) - let dt = dateTime(year, Month(month), MonthdayRange(dayOfMonth), - HourRange(time[0]), MinuteRange(time[1]), SecondRange(time[2])) - return some(dt.toTime().toUnix()) - -# For debugging -proc `$`*(cookieJar: CookieJar): string = - result = $cookieJar.domain - result &= ":\n" - for cookie in cookieJar.cookies: - result &= "Cookie " - result &= $cookie[] - result &= "\n" - -# https://www.rfc-editor.org/rfc/rfc6265#section-5.1.4 -func defaultCookiePath(url: URL): string = - let path = url.pathname.untilLast('/') - if path == "" or path[0] != '/': - return "/" - return path - -func cookiePathMatches(cookiePath, requestPath: string): bool = - if requestPath.startsWith(cookiePath): - if requestPath.len == cookiePath.len: - return true - if cookiePath[^1] == '/': - return true - if requestPath.len > cookiePath.len and requestPath[cookiePath.len] == '/': - return true - return false - -# I have no clue if this is actually compliant, because the spec is worded -# so badly. -# Either way, this implementation is needed for compatibility. -# (Here is this part of the spec in its full glory: -# A string domain-matches a given domain string if at least one of the -# following conditions hold: -# o The domain string and the string are identical. (Note that both -# the domain string and the string will have been canonicalized to -# lower case at this point.) -# o All of the following conditions hold: -# * The domain string is a suffix of the string. -# * The last character of the string that is not included in the -# domain string is a %x2E (".") character. (???) -# * The string is a host name (i.e., not an IP address).) -func cookieDomainMatches(cookieDomain: string; url: URL): bool = - if cookieDomain.len == 0: - return false - let host = url.host - if host == cookieDomain: - return true - if url.isIP(): - return false - let cookieDomain = if cookieDomain[0] == '.': - cookieDomain.substr(1) - else: - cookieDomain - return host.endsWith(cookieDomain) - -proc add(cookieJar: CookieJar; cookie: Cookie) = - var i = -1 - for j, old in cookieJar.cookies.mypairs: - if old.name == cookie.name and old.domain == cookie.domain and - old.path == cookie.path: - i = j - break - if i != -1: - cookieJar.cookies[i] = cookie - else: - cookieJar.cookies.add(cookie) - -# https://www.rfc-editor.org/rfc/rfc6265#section-5.4 -proc serialize*(cookieJar: CookieJar; url: URL): string = - var res = "" - let t = getTime().toUnix() - #TODO sort - 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) - continue - if cookie.secure and url.scheme != "https": - continue - if not cookiePathMatches(cookie.path, url.pathname): - continue - if cookie.hostOnly and cookie.domain != url.host: - continue - if not cookie.hostOnly and not cookieDomainMatches(cookie.domain, url): - continue - if res != "": - res &= "; " - res &= cookie.name - res &= "=" - res &= cookie.value - return res - -proc parseCookie(str: string; t: int64; url: URL): Opt[Cookie] = - let cookie = Cookie(expires: -1, hostOnly: true) - var first = true - var hasPath = false - for part in str.split(';'): - if first: - cookie.name = part.until('=') - cookie.value = part.after('=') - first = false - continue - let part = part.strip(leading = true, trailing = false, AsciiWhitespace) - let n = part.find('=') - if n <= 0: - continue - let key = part.substr(0, n - 1) - let val = part.substr(n + 1) - case key.toLowerAscii() - of "expires": - if cookie.expires == -1: - let date = parseCookieDate(val) - if date.isSome: - cookie.expires = date.get - of "max-age": - let x = parseInt64(val) - if x.isSome: - cookie.expires = t + x.get - of "secure": cookie.secure = true - of "httponly": cookie.httponly = true - of "path": - if val != "" and val[0] == '/': - hasPath = true - cookie.path = val - of "domain": - if not cookieDomainMatches(val, url): - return err() - cookie.domain = val - cookie.hostOnly = false - if cookie.hostOnly: - cookie.domain = url.host - if not hasPath: - cookie.path = defaultCookiePath(url) - return ok(cookie) - -proc newCookieJar*(url: URL): CookieJar = - return CookieJar(domain: url.host) - -proc setCookie*(cookieJar: CookieJar; header: openArray[string]; url: URL) = - let t = getTime().toUnix() - for s in header: - let cookie = parseCookie(s, t, url) - if cookie.isSome: - cookieJar.add(cookie.get) diff --git a/src/types/url.nim b/src/types/url.nim index c2d6e075..aa7d9f23 100644 --- a/src/types/url.nim +++ b/src/types/url.nim @@ -25,7 +25,7 @@ type usFail, usDone, usSchemeStart, usNoScheme, usFile, usFragment, usAuthority, usPath, usQuery, usHost, usHostname, usPort, usPathStart - HostType = enum + HostType* = enum htNone, htDomain, htIpv4, htIpv6, htOpaque URLSearchParams* = ref object @@ -390,7 +390,7 @@ proc domainToAscii(domain: string; bestrict = false): string = return domain.unicodeToAscii(bestrict) return domain.toLowerAscii() -proc parseHost(input: string; special: bool; hostType: var HostType): string = +proc parseHost*(input: string; special: bool; hostType: var HostType): string = if input.len == 0: return "" if input[0] == '[': |