about summary refs log blame commit diff stats
path: root/src/types/cookie.nim
blob: 41de0f4cbbc56c24b1f449dd1a16d74f22e4a07a (plain) (tree)
1
2
3
4
5
6
7
8
9
10



               
                   
                    
               
                

                   












                                        
                      
                         





















































































                                                                                                                                        







                                         


                                                              


                                                
                                                    
                                 
                                                                      





                                                                  
                          
                     





















                                                                                           

                                                                






                                         
                                                                      
                   


                                      
                             
     

   

                                       
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)