about summary refs log blame commit diff stats
path: root/src/loader/headers.nim
blob: e09d22672a453a5f77a3243b3475f1548647175a (plain) (tree)
1
2
3
4
5
6
7
8
9
                   
                 
 



                          
                    
                

                   
    






                                       
                       

                                      

                        
                         


                           
                   
                              
                
                                


                     

                                              
                                                                            
                                           




                                             
                  

                          
                         





                                     
 














































                                                                              

                                               

















                                                                       

























                                                                               
                                                                            
                                  


                                                 
                           

                                  










                                                                            
                                




                                              



                               



                                                                     
                                










                                                                              
                                







                                                                          
                                            





                                                                       
 
                                                                         
                  

                         
 



                                                                 
 

                                           
 

                                                                        
                

                          
 



                                                               


                                   


                             
                                                         
                         

                            


                                   

                             
 
                                        
                                      
 
                                           
                          


                                
                           
 
                                                            

                            
 
                                                            


                            
                                                          


                            
                                                                              




                                


                                        
import std/strutils
import std/tables

import monoucha/fromjs
import monoucha/javascript
import monoucha/jserror
import monoucha/quickjs
import monoucha/tojs
import types/opt
import utils/twtstr

type
  HeaderGuard* = enum
    hgNone = "none"
    hgImmutable = "immutable"
    hgRequest = "request"
    hgRequestNoCors = "request-no-cors"
    hgResponse = "response"

  Headers* = ref object
    table*: Table[string, seq[string]]
    guard*: HeaderGuard

  HeadersInitType = enum
    hitSequence, hitTable

  HeadersInit* = object
    case t: HeadersInitType
    of hitSequence:
      s: seq[(string, string)]
    of hitTable:
      tab: Table[string, string]

jsDestructor(Headers)

const HTTPWhitespace = {'\n', '\r', '\t', ' '}

proc fromJS(ctx: JSContext; val: JSValue; res: var HeadersInit): Err[void] =
  if JS_IsUndefined(val) or JS_IsNull(val):
    return err()
  var headers: Headers
  if ctx.fromJS(val, headers).isSome:
    res = HeadersInit(t: hitSequence, s: @[])
    for k, v in headers.table:
      for vv in v:
        res.s.add((k, vv))
    return ok()
  if ctx.isSequence(val):
    res = HeadersInit(t: hitSequence)
    if ctx.fromJS(val, res.s).isSome:
      return ok()
  res = HeadersInit(t: hitTable)
  ?ctx.fromJS(val, res.tab)
  return ok()

const TokenChars = {
  '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~'
} + AsciiAlphaNumeric

func isValidHeaderName*(s: string): bool =
  return s.len > 0 and AllChars - TokenChars notin s

func isValidHeaderValue*(s: string): bool =
  return s.len == 0 or s[0] notin {' ', '\t'} and s[^1] notin {' ', '\t'} and
    '\n' notin s

func isForbiddenRequestHeader*(name, value: string): bool =
  const ForbiddenNames = [
    "Accept-Charset",
    "Accept-Encoding",
    "Access-Control-Request-Headers",
    "Access-Control-Request-Method",
    "Connection",
    "Content-Length",
    "Cookie",
    "Cookie2",
    "Date",
    "DNT",
    "Expect",
    "Host",
    "Keep-Alive",
    "Origin",
    "Referer",
    "Set-Cookie",
    "TE",
    "Trailer",
    "Transfer-Encoding",
    "Upgrade",
    "Via"
  ]
  for x in ForbiddenNames:
    if name.equalsIgnoreCase(x):
      return true
  if name.startsWithIgnoreCase("proxy-") or name.startsWithIgnoreCase("sec-"):
    return true
  if name.equalsIgnoreCase("X-HTTP-Method") or
      name.equalsIgnoreCase("X-HTTP-Method-Override") or
      name.equalsIgnoreCase("X-Method-Override"):
    return true # meh
  return false

func isForbiddenResponseHeaderName*(name: string): bool =
  return name.equalsIgnoreCase("Set-Cookie") or
    name.equalsIgnoreCase("Set-Cookie2")

proc validate(this: Headers; name, value: string): JSResult[bool] =
  if not name.isValidHeaderName() or not value.isValidHeaderValue():
    return errTypeError("Invalid header name or value")
  if this.guard == hgImmutable:
    return errTypeError("Tried to modify immutable Headers object")
  if this.guard == hgRequest and isForbiddenRequestHeader(name, value):
    return ok(false)
  if this.guard == hgResponse and name.isForbiddenResponseHeaderName():
    return ok(false)
  return ok(true)

func isNoCorsSafelistedName(name: string): bool =
  return name.equalsIgnoreCase("Accept") or
    name.equalsIgnoreCase("Accept-Language") or
    name.equalsIgnoreCase("Content-Language") or
    name.equalsIgnoreCase("Content-Type")

const CorsUnsafeRequestByte = {
  char(0x00)..char(0x08), char(0x10)..char(0x1F), '"', '(', ')', ':', '<', '>',
  '?', '@', '[', '\\', ']', '{', '}', '\e'
}

func isNoCorsSafelisted(name, value: string): bool =
  if value.len > 128:
    return false
  if name.equalsIgnoreCase("Accept"):
    return CorsUnsafeRequestByte notin value
  if name.equalsIgnoreCase("Accept-Language") or
      name.equalsIgnoreCase("Content-Language"):
    const Forbidden = AllChars - AsciiAlphaNumeric -
      {' ', '*', ',', '-', '.', ';', '='}
    return Forbidden notin value
  if name.equalsIgnoreCase("Content-Type"):
    return value.strip(chars = AsciiWhitespace).toLowerAscii() in [
      "multipart/form-data",
      "text/plain",
      "application-x-www-form-urlencoded"
    ]
  return false

func get0(this: Headers; name: string): string =
  return this.table[name].join(", ")

proc get*(ctx: JSContext; this: Headers; name: string): JSValue {.jsfunc.} =
  if not name.isValidHeaderName():
    JS_ThrowTypeError(ctx, "Invalid header name")
    return JS_EXCEPTION
  let name = name.toHeaderCase()
  if name notin this.table:
    return JS_NULL
  return ctx.toJS(this.get0(name))

proc removeRange(this: Headers) =
  if this.guard == hgRequestNoCors:
    #TODO do this case insensitively
    this.table.del("Range") # privileged no-CORS request headers
    this.table.del("range")

proc append(this: Headers; name, value: string): JSResult[void] {.jsfunc.} =
  let value = value.strip(chars = HTTPWhitespace)
  if not ?this.validate(name, value):
    return ok()
  let name = name.toHeaderCase()
  if this.guard == hgRequestNoCors:
    if name in this.table:
      let tmp = this.get0(name) & ", " & value
      if not name.isNoCorsSafelisted(tmp):
        return ok()
  if name in this.table:
    this.table[name].add(value)
  else:
    this.table[name] = @[value]
  this.removeRange()
  ok()

proc delete(this: Headers; name: string): JSResult[void] {.jsfunc.} =
  let name = name.toHeaderCase()
  if not ?this.validate(name, "") or name notin this.table:
    return ok()
  if not name.isNoCorsSafelistedName() and not name.equalsIgnoreCase("Range"):
    return ok()
  this.table.del(name)
  this.removeRange()
  ok()

proc has(this: Headers; name: string): JSResult[bool] {.jsfunc.} =
  if not name.isValidHeaderName():
    return errTypeError("Invalid header name")
  let name = name.toHeaderCase()
  return ok(name in this.table)

proc set(this: Headers; name, value: string): JSResult[void] {.jsfunc.} =
  let value = value.strip(chars = HTTPWhitespace)
  if not ?this.validate(name, value):
    return
  if this.guard == hgRequestNoCors and not name.isNoCorsSafelisted(value):
    return
  this.table[name.toHeaderCase()] = @[value]
  this.removeRange()

proc fill(headers: Headers; s: seq[(string, string)]): JSResult[void] =
  for (k, v) in s:
    ?headers.append(k, v)
  ok()

proc fill(headers: Headers; tab: Table[string, string]): JSResult[void] =
  for k, v in tab:
    ?headers.append(k, v)
  ok()

proc fill*(headers: Headers; init: HeadersInit): JSResult[void] =
  case init.t
  of hitSequence: return headers.fill(init.s)
  of hitTable: return headers.fill(init.tab)

func newHeaders*(guard = hgNone): Headers =
  return Headers(guard: guard)

func newHeaders(obj = none(HeadersInit)): JSResult[Headers] {.jsctor.} =
  let headers = Headers(guard: hgNone)
  if obj.isSome:
    ?headers.fill(obj.get)
  return ok(headers)

func newHeaders*(table: openArray[(string, string)]): Headers =
  let headers = Headers()
  for (k, v) in table:
    let k = k.toHeaderCase()
    headers.table.withValue(k, vs):
      vs[].add(v)
    do:
      headers.table[k] = @[v]
  return headers

func newHeaders*(table: Table[string, string]): Headers =
  let headers = Headers()
  for k, v in table:
    let k = k.toHeaderCase()
    headers.table.withValue(k, vs):
      vs[].add(v)
    do:
      headers.table[k] = @[v]
  return headers

func clone*(headers: Headers): Headers =
  return Headers(table: headers.table)

proc add*(headers: Headers; k, v: string) =
  let k = k.toHeaderCase()
  headers.table.withValue(k, p):
    p[].add(v)
  do:
    headers.table[k] = @[v]

proc `[]=`*(headers: Headers; k: static string, v: string) =
  const k = k.toHeaderCase()
  headers.table[k] = @[v]

func `[]`*(headers: Headers; k: static string): var string =
  const k = k.toHeaderCase()
  return headers.table[k][0]

func contains*(headers: Headers; k: static string): bool =
  const k = k.toHeaderCase()
  return k in headers.table

func getOrDefault*(headers: Headers; k: static string; default = ""): string =
  const k = k.toHeaderCase()
  headers.table.withValue(k, p):
    return p[][0]
  do:
    return default

proc addHeadersModule*(ctx: JSContext) =
  ctx.registerType(Headers)