about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/html/dom.nim20
-rw-r--r--src/html/xmlhttprequest.nim9
-rw-r--r--src/local/container.nim6
-rw-r--r--src/local/pager.nim10
-rw-r--r--src/server/buffer.nim2
-rw-r--r--src/server/headers.nim66
-rw-r--r--src/server/loader.nim18
-rw-r--r--src/server/loaderiface.nim22
-rw-r--r--src/server/request.nim12
-rw-r--r--src/server/response.nim23
10 files changed, 101 insertions, 87 deletions
diff --git a/src/html/dom.nim b/src/html/dom.nim
index db192977..1eeac6c0 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -627,7 +627,7 @@ proc create2DContext(jctx: JSContext; target: HTMLCanvasElement;
   let request = newRequest(
     newURL("img-codec+x-cha-canvas:decode").get,
     httpMethod = hmPost,
-    headers = newHeaders({"Cha-Image-Info-Only": "1"}),
+    headers = newHeaders(hgRequest, {"Cha-Image-Info-Only": "1"}),
     body = RequestBody(t: rbtOutput, outputId: ctlres.outputId)
   )
   let response = loader.doRequest(request)
@@ -4263,7 +4263,7 @@ proc loadResource*(window: Window; image: HTMLImageElement) =
         return
     let cachedURL = CachedURLImage(expiry: -1, loading: true)
     window.imageURLCache[surl] = cachedURL
-    let headers = newHeaders({"Accept": "*/*"})
+    let headers = newHeaders(hgRequest, {"Accept": "*/*"})
     let p = window.corsFetch(newRequest(url, headers = headers)).then(
       proc(res: JSResult[Response]): EmptyPromise =
         if res.isNone:
@@ -4293,7 +4293,7 @@ proc loadResource*(window: Window; image: HTMLImageElement) =
         let request = newRequest(
           url.get,
           httpMethod = hmPost,
-          headers = newHeaders({"Cha-Image-Info-Only": "1"}),
+          headers = newHeaders(hgRequest, {"Cha-Image-Info-Only": "1"}),
           body = RequestBody(t: rbtOutput, outputId: response.outputId),
         )
         let r = window.corsFetch(request)
@@ -4301,14 +4301,14 @@ proc loadResource*(window: Window; image: HTMLImageElement) =
         response.close()
         var expiry = -1i64
         if "Cache-Control" in response.headers:
-          for hdr in response.headers.table["Cache-Control"]:
-            var i = hdr.find("max-age=")
-            if i != -1:
-              i = hdr.skipBlanks(i + "max-age=".len)
+          for hdr in response.headers.getAllCommaSplit("Cache-Control"):
+            let s = hdr.strip()
+            if s.startsWithIgnoreCase("max-age="):
+              let i = hdr.skipBlanks("max-age=".len)
               let s = hdr.until(AllChars - AsciiDigit, i)
               let pi = parseInt64(s)
               if pi.isSome:
-                expiry = getTime().utc().toTime().toUnix() + pi.get
+                expiry = getTime().toUnix() + pi.get
               break
         cachedURL.loading = false
         cachedURL.expiry = expiry
@@ -4378,7 +4378,7 @@ proc loadResource*(window: Window; svg: SVGSVGElement) =
   let request = newRequest(
     newURL("img-codec+svg+xml:decode").get,
     httpMethod = hmPost,
-    headers = newHeaders({"Cha-Image-Info-Only": "1"}),
+    headers = newHeaders(hgRequest, {"Cha-Image-Info-Only": "1"}),
     body = RequestBody(t: rbtOutput, outputId: svgres.outputId)
   )
   let p = loader.fetch(request).then(proc(res: JSResult[Response]) =
@@ -6016,7 +6016,7 @@ proc toBlob(ctx: JSContext; this: HTMLCanvasElement; callback: JSValueConst;
   if url0.isNone:
     return
   let url = url0.get
-  let headers = newHeaders({
+  let headers = newHeaders(hgRequest, {
     "Cha-Image-Dimensions": $this.bitmap.width & 'x' & $this.bitmap.height
   })
   if (var quality = quality.get(-1); 0 <= quality and quality <= 1):
diff --git a/src/html/xmlhttprequest.nim b/src/html/xmlhttprequest.nim
index 556d32b1..c24fa262 100644
--- a/src/html/xmlhttprequest.nim
+++ b/src/html/xmlhttprequest.nim
@@ -1,6 +1,5 @@
 import std/options
 import std/strutils
-import std/tables
 
 import chagashi/charset
 import chagashi/decoder
@@ -83,7 +82,7 @@ func newXMLHttpRequest(): XMLHttpRequest {.jsctor.} =
   let upload = XMLHttpRequestUpload()
   return XMLHttpRequest(
     upload: upload,
-    headers: newHeaders(),
+    headers: newHeaders(hgRequest),
     responseObject: JS_UNDEFINED
   )
 
@@ -149,7 +148,7 @@ proc open(ctx: JSContext; this: XMLHttpRequest; httpMethod, url: string;
   else:
     this.flags.incl(xhrfSync)
   this.requestMethod = httpMethod
-  this.headers = newHeaders()
+  this.headers = newHeaders(hgRequest)
   this.response = makeNetworkError()
   this.received = ""
   this.requestURL = parsedURL
@@ -178,7 +177,7 @@ proc setRequestHeader(this: XMLHttpRequest; name, value: string):
     return errDOMException("Invalid header name or value", "SyntaxError")
   if isForbiddenRequestHeader(name, value):
     return ok()
-  this.headers.table[name.toHeaderCase()] = @[value]
+  this.headers[name] = value
   ok()
 
 proc `withCredentials=`(this: XMLHttpRequest; withCredentials: bool):
@@ -383,7 +382,7 @@ proc getResponseHeader(ctx: JSContext; this: XMLHttpRequest; name: string):
 proc getAllResponseHeaders(this: XMLHttpRequest): string {.jsfunc.} =
   result = ""
   #TODO sort, should use the filtered header list, etc.
-  for k, v in this.response.headers.table:
+  for k, v in this.response.headers:
     if k.isForbiddenResponseHeaderName():
       continue
     for it in v:
diff --git a/src/local/container.nim b/src/local/container.nim
index 3c1423f1..3b25f14f 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -1541,9 +1541,9 @@ proc applyResponse*(container: Container; response: Response;
     mimeTypes: MimeTypes) =
   # 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,
-      container.config.cookieMode == cmSave)
+  if cookieJar != nil:
+    cookieJar.setCookie(response.headers.getAllNoComma("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 3fe65ee5..ac78f8bb 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -450,7 +450,7 @@ proc newPager*(config: Config; forkserver: ForkServer; ctx: JSContext;
   JS_SetModuleLoaderFunc(pager.jsrt, normalizeModuleName, loadJSModule, nil)
   JS_SetInterruptHandler(pager.jsrt, interruptHandler, nil)
   let clientConfig = LoaderClientConfig(
-    defaultHeaders: newHeaders(pager.config.network.defaultHeaders),
+    defaultHeaders: newHeaders(hgRequest, pager.config.network.defaultHeaders),
     proxy: pager.config.network.proxy,
     filter: newURLFilter(default = true),
   )
@@ -1052,7 +1052,7 @@ proc loadCachedImage(pager: Pager; container: Container; image: PosBitmap;
       return newResolvedPromise(res)
     # resize
     # use a temp file, so that img-resize can mmap its output
-    let headers = newHeaders({
+    let headers = newHeaders(hgRequest, {
       "Cha-Image-Dimensions": $bmp.width & 'x' & $bmp.height,
       "Cha-Image-Target-Dimensions": $image.width & 'x' & $image.height
     })
@@ -1078,7 +1078,7 @@ proc loadCachedImage(pager: Pager; container: Container; image: PosBitmap;
     if cachedImage.state == cisCanceled:
       pager.loader.removeCachedItem(cacheId)
       return
-    let headers = newHeaders({
+    let headers = newHeaders(hgRequest, {
       "Cha-Image-Dimensions": $image.width & 'x' & $image.height
     })
     var url: URL = nil
@@ -1790,7 +1790,7 @@ proc applySiteconf(pager: Pager; url: URL; charsetOverride: Charset;
   )
   loaderConfig = LoaderClientConfig(
     originURL: url,
-    defaultHeaders: newHeaders(pager.config.network.defaultHeaders),
+    defaultHeaders: newHeaders(hgRequest, pager.config.network.defaultHeaders),
     cookiejar: nil,
     proxy: pager.config.network.proxy,
     filter: newURLFilter(
@@ -1847,7 +1847,7 @@ proc applySiteconf(pager: Pager; url: URL; charsetOverride: Charset;
     if sc.proxy.isSome:
       loaderConfig.proxy = sc.proxy.get
     if sc.defaultHeaders != nil:
-      loaderConfig.defaultHeaders = newHeaders(sc.defaultHeaders[])
+      loaderConfig.defaultHeaders = newHeaders(hgRequest, sc.defaultHeaders[])
     if sc.insecureSslNoVerify.isSome:
       loaderConfig.insecureSslNoVerify = sc.insecureSslNoVerify.get
     if sc.autofocus.isSome:
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index 2e8b95b4..403462c0 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -1274,7 +1274,7 @@ proc makeFormRequest(buffer: Buffer; parsedAction: URL; httpMethod: HttpMethod;
       #TODO with charset
       let kvlist = entryList.toNameValuePairs()
       RequestBody(t: rbtString, s: serializePlainTextFormData(kvlist))
-    let headers = newHeaders({"Content-Type": $enctype})
+    let headers = newHeaders(hgRequest, {"Content-Type": $enctype})
     return newRequest(parsedAction, httpMethod, headers, body)
 
 # https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
diff --git a/src/server/headers.nim b/src/server/headers.nim
index 2ed49e04..25102486 100644
--- a/src/server/headers.nim
+++ b/src/server/headers.nim
@@ -18,7 +18,7 @@ type
     hgResponse = "response"
 
   Headers* = ref object
-    table*: Table[string, seq[string]]
+    table: Table[string, seq[string]]
     guard*: HeaderGuard
 
   HeadersInitType = enum
@@ -33,6 +33,20 @@ type
 
 jsDestructor(Headers)
 
+func isForbiddenResponseHeaderName*(name: string): bool
+
+iterator pairs*(this: Headers): (string, string) =
+  for k, vs in this.table:
+    if this.guard == hgResponse and k.isForbiddenResponseHeaderName():
+      continue
+    for v in vs:
+      yield (k, v)
+
+iterator allPairs*(headers: Headers): (string, string) =
+  for k, vs in headers.table:
+    for v in vs:
+      yield (k, v)
+
 const HTTPWhitespace = {'\n', '\r', '\t', ' '}
 
 proc fromJS(ctx: JSContext; val: JSValueConst; res: var HeadersInit):
@@ -219,17 +233,12 @@ proc fill*(headers: Headers; init: HeadersInit): JSResult[void] =
   of hitSequence: return headers.fill(init.s)
   of hitTable: return headers.fill(init.tab)
 
-func newHeaders*(guard = hgNone): Headers =
+func newHeaders*(guard: HeaderGuard): 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()
+func newHeaders*(guard: HeaderGuard; table: openArray[(string, string)]):
+    Headers =
+  let headers = newHeaders(guard)
   for (k, v) in table:
     let k = k.toHeaderCase()
     headers.table.withValue(k, vs):
@@ -238,8 +247,8 @@ func newHeaders*(table: openArray[(string, string)]): Headers =
       headers.table[k] = @[v]
   return headers
 
-func newHeaders*(table: Table[string, string]): Headers =
-  let headers = Headers()
+func newHeaders*(guard: HeaderGuard; table: Table[string, string]): Headers =
+  let headers = newHeaders(guard)
   for k, v in table:
     let k = k.toHeaderCase()
     headers.table.withValue(k, vs):
@@ -248,6 +257,12 @@ func newHeaders*(table: Table[string, string]): Headers =
       headers.table[k] = @[v]
   return headers
 
+func newHeaders(obj = none(HeadersInit)): JSResult[Headers] {.jsctor.} =
+  let headers = Headers(guard: hgNone)
+  if obj.isSome:
+    ?headers.fill(obj.get)
+  return ok(headers)
+
 func clone*(headers: Headers): Headers =
   return Headers(table: headers.table)
 
@@ -258,24 +273,33 @@ proc add*(headers: Headers; k: string; v: sink string) =
   do:
     headers.table[k] = @[v]
 
-proc `[]=`*(headers: Headers; k: static string; v: string) =
-  const k = k.toHeaderCase()
+proc `[]=`*(headers: Headers; k: string; v: sink string) =
+  let k = k.toHeaderCase()
   headers.table[k] = @[v]
 
-func `[]`*(headers: Headers; k: static string): var string =
-  const k = k.toHeaderCase()
+func `[]`*(headers: Headers; k: string): var string =
+  let 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 contains*(headers: Headers; k: string): bool =
+  return k.toHeaderCase() in headers.table
 
-func getOrDefault*(headers: Headers; k: static string; default = ""): string =
-  const k = k.toHeaderCase()
+func getOrDefault*(headers: Headers; k: string; default = ""): string =
+  let k = k.toHeaderCase()
   headers.table.withValue(k, p):
     return p[][0]
   do:
     return default
 
+func getAllCommaSplit*(headers: Headers; k: string): seq[string] =
+  headers.table.withValue(k, p):
+    return p[].join(",").split(',')
+  return @[]
+
+func getAllNoComma*(headers: Headers; k: string): seq[string] =
+  headers.table.withValue(k, p):
+    return p[]
+  return @[]
+
 proc addHeadersModule*(ctx: JSContext) =
   ctx.registerType(Headers)
diff --git a/src/server/loader.nim b/src/server/loader.nim
index 5f993ccc..92a10fd6 100644
--- a/src/server/loader.nim
+++ b/src/server/loader.nim
@@ -804,7 +804,7 @@ proc setupEnv(cpath: CGIPath; request: Request; contentLen: int; prevURL: URL;
   putEnv("REQUEST_URI", cpath.requestURI)
   putEnv("REQUEST_METHOD", $request.httpMethod)
   var headers = ""
-  for k, v in request.headers:
+  for k, v in request.headers.allPairs:
     headers &= k & ": " & v & "\r\n"
   putEnv("REQUEST_HEADERS", headers)
   if prevURL != nil:
@@ -971,7 +971,7 @@ proc loadCGI(ctx: var LoaderContext; client: ClientHandle; handle: InputHandle;
       ostreamOut2.sclose() # close write
     if request.body.t != rbtNone:
       istream.sclose() # close read
-    handle.parser = HeaderParser(headers: newHeaders())
+    handle.parser = HeaderParser(headers: newHeaders(hgResponse))
     handle.stream = istreamOut
     case request.body.t
     of rbtString:
@@ -1015,7 +1015,7 @@ proc loadStream(ctx: var LoaderContext; client: ClientHandle;
   case ctx.sendResult(handle, 0)
   of pbrDone: discard
   of pbrUnregister: return
-  case ctx.sendStatus(handle, 200, newHeaders())
+  case ctx.sendStatus(handle, 200, newHeaders(hgResponse))
   of pbrDone: discard
   of pbrUnregister: return
   let ps = client.passedFdMap[i].ps
@@ -1049,7 +1049,7 @@ proc loadFromCache(ctx: var LoaderContext; client: ClientHandle;
       client.cacheMap.del(n)
       ctx.close(handle)
       return
-    case ctx.sendStatus(handle, 200, newHeaders())
+    case ctx.sendStatus(handle, 200, newHeaders(hgResponse))
     of pbrDone: discard
     of pbrUnregister:
       client.cacheMap.del(n)
@@ -1070,7 +1070,7 @@ proc loadDataSend(ctx: var LoaderContext; handle: InputHandle; s, ct: string) =
   of pbrUnregister:
     ctx.close(handle)
     return
-  case ctx.sendStatus(handle, 200, newHeaders({"Content-Type": ct}))
+  case ctx.sendStatus(handle, 200, newHeaders(hgResponse, {"Content-Type": ct}))
   of pbrDone: discard
   of pbrUnregister:
     ctx.close(handle)
@@ -1309,11 +1309,11 @@ proc loadResource(ctx: var LoaderContext; client: ClientHandle;
     ctx.rejectHandle(handle, ceTooManyRewrites)
 
 proc setupRequestDefaults(request: Request; config: LoaderClientConfig) =
-  for k, v in config.defaultHeaders.table:
-    if k notin request.headers.table:
-      request.headers.table[k] = v
+  for k, v in config.defaultHeaders.allPairs:
+    if k notin request.headers:
+      request.headers[k] = v
   if config.cookieJar != nil and config.cookieJar.cookies.len > 0:
-    if "Cookie" notin request.headers.table:
+    if "Cookie" notin request.headers:
       let cookie = config.cookieJar.serialize(request.url)
       if cookie != "":
         request.headers["Cookie"] = cookie
diff --git a/src/server/loaderiface.nim b/src/server/loaderiface.nim
index 0afd8fde..ba6cf9f9 100644
--- a/src/server/loaderiface.nim
+++ b/src/server/loaderiface.nim
@@ -86,18 +86,16 @@ type
     insecureSslNoVerify*: bool
 
 proc getRedirect*(response: Response; request: Request): Request =
-  if "Location" in response.headers.table:
-    if response.status in 301u16..303u16 or response.status in 307u16..308u16:
-      let location = response.headers.table["Location"][0]
-      let url = parseURL(location, option(request.url))
-      if url.isSome:
-        let status = response.status
-        if status == 303 and request.httpMethod notin {hmGet, hmHead} or
-            status == 301 or
-            status == 302 and request.httpMethod == hmPost:
-          return newRequest(url.get, hmGet)
-        else:
-          return newRequest(url.get, request.httpMethod, body = request.body)
+  if response.status in 301u16..303u16 or response.status in 307u16..308u16:
+    let location = response.headers.getOrDefault("Location")
+    let url = parseURL(location, option(request.url))
+    if url.isSome:
+      let status = response.status
+      if status == 303 and request.httpMethod notin {hmGet, hmHead} or
+          status == 301 or
+          status == 302 and request.httpMethod == hmPost:
+        return newRequest(url.get, hmGet)
+      return newRequest(url.get, request.httpMethod, body = request.body)
   return nil
 
 template withPacketWriter(loader: FileLoader; w, body: untyped) =
diff --git a/src/server/request.nim b/src/server/request.nim
index 62dbc45c..31b4c117 100644
--- a/src/server/request.nim
+++ b/src/server/request.nim
@@ -1,5 +1,4 @@
 import std/options
-import std/tables
 
 import html/script
 import io/packetreader
@@ -137,12 +136,7 @@ proc jsReferrer(this: JSRequest): string {.jsfget: "referrer".} =
     return $this.request.referrer
   return ""
 
-iterator pairs*(headers: Headers): (string, string) =
-  for k, vs in headers.table:
-    for v in vs:
-      yield (k, v)
-
-func newRequest*(url: URL; httpMethod = hmGet; headers = newHeaders();
+func newRequest*(url: URL; httpMethod = hmGet; headers = newHeaders(hgRequest);
     body = RequestBody(); referrer: URL = nil; tocache = false): Request =
   return Request(
     url: url,
@@ -216,7 +210,7 @@ var getAPIBaseURLImpl*: proc(ctx: JSContext): URL {.nimcall.}
 
 proc newRequest*(ctx: JSContext; resource: JSValueConst;
     init = RequestInit(window: JS_UNDEFINED)): JSResult[JSRequest] {.jsctor.} =
-  let headers = newHeaders(hgRequest)
+  var headers = newHeaders(hgRequest)
   var fallbackMode = opt(rmCors)
   var window = RequestWindow(t: rwtClient)
   var body = RequestBody()
@@ -227,7 +221,7 @@ proc newRequest*(ctx: JSContext; resource: JSValueConst;
   if (var res: JSRequest; ctx.fromJS(resource, res).isSome):
     url = res.url
     httpMethod = res.request.httpMethod
-    headers.table = res.headers.table
+    headers[] = res.headers[]
     referrer = res.request.referrer
     credentials = res.credentialsMode
     body = res.request.body
diff --git a/src/server/response.nim b/src/server/response.nim
index 58fc0571..401ce030 100644
--- a/src/server/response.nim
+++ b/src/server/response.nim
@@ -1,6 +1,5 @@
 import std/posix
 import std/strutils
-import std/tables
 
 import chagashi/charset
 import chagashi/decoder
@@ -104,16 +103,17 @@ proc close*(response: Response) =
     response.body = nil
 
 func getCharset*(this: Response; fallback: Charset): Charset =
-  this.headers.table.withValue("Content-Type", p):
-    let header = p[][0].toLowerAscii()
+  let header = this.headers.getOrDefault("Content-Type").toLowerAscii()
+  if header != "":
     let cs = header.getContentTypeAttr("charset").getCharset()
     if cs != CHARSET_UNKNOWN:
       return cs
   return fallback
 
 func getLongContentType*(this: Response; fallback: string): string =
-  this.headers.table.withValue("Content-Type", p):
-    return p[][0].toValidUTF8().toLowerAscii().strip()
+  let header = this.headers.getOrDefault("Content-Type")
+  if header != "":
+    return header.toValidUTF8().toLowerAscii().strip()
   # also use DefaultGuess for container, so that local mime.types cannot
   # override buffer mime.types
   return DefaultGuess.guessContentType(this.url.pathname, fallback)
@@ -123,16 +123,15 @@ func getContentType*(this: Response; fallback = "application/octet-stream"):
   return this.getLongContentType(fallback).until(';')
 
 func getContentLength*(this: Response): int64 =
-  this.headers.table.withValue("Content-Length", p):
-    for x in p[]:
-      let u = parseUInt64(x.strip(), allowSign = false)
-      if u.isSome and u.get <= uint64(int64.high):
-        return int64(u.get)
+  let x = this.headers.getOrDefault("Content-Length")
+  let u = parseUInt64(x.strip(), allowSign = false)
+  if u.isSome and u.get <= uint64(int64.high):
+    return int64(u.get)
   return -1
 
 func getReferrerPolicy*(this: Response): Option[ReferrerPolicy] =
-  this.headers.table.withValue("Referrer-Policy", p):
-    return strictParseEnum[ReferrerPolicy](p[][0])
+  let header = this.headers.getOrDefault("Referrer-Policy")
+  return strictParseEnum[ReferrerPolicy](header)
 
 proc resume*(response: Response) =
   response.resumeFun(response.outputId)