about summary refs log tree commit diff stats
path: root/src/io
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2023-08-13 17:42:34 +0200
committerbptato <nincsnevem662@gmail.com>2023-08-13 17:54:05 +0200
commitd526deb99e44f2a8d1a9c3eea60676703dd64302 (patch)
treef63689ff7654d14ad9bca182a837b3155b2471a0 /src/io
parentf92e30232252deb194596e7c298cc7fcf56517cb (diff)
downloadchawan-d526deb99e44f2a8d1a9c3eea60676703dd64302.tar.gz
Add mailcap, mime.types & misc refactorings
* add mailcap: works with copiousoutput, needsterminal, etc.
* add mime.types (only works with mailcap)
* refactor pipeBuffer
* remove "dispatcher"
* fix bug in directory display where baseurl would not be used
Diffstat (limited to 'src/io')
-rw-r--r--src/io/about.nim29
-rw-r--r--src/io/connecterror.nim17
-rw-r--r--src/io/file.nim94
-rw-r--r--src/io/http.nim75
-rw-r--r--src/io/loader.nim146
-rw-r--r--src/io/loaderhandle.nim73
-rw-r--r--src/io/posixstream.nim5
-rw-r--r--src/io/request.nim7
-rw-r--r--src/io/response.nim2
-rw-r--r--src/io/tempfile.nim18
10 files changed, 302 insertions, 164 deletions
diff --git a/src/io/about.nim b/src/io/about.nim
index 97e01133..737a291b 100644
--- a/src/io/about.nim
+++ b/src/io/about.nim
@@ -1,9 +1,9 @@
-import streams
 import tables
 
+import io/connecterror
 import io/headers
+import io/loaderhandle
 import io/request
-import ips/serialize
 import types/url
 
 const chawan = staticRead"res/chawan.html"
@@ -11,19 +11,18 @@ const HeaderTable = {
   "Content-Type": "text/html"
 }.toTable()
 
-proc loadAbout*(request: Request, ostream: Stream) =
+proc loadAbout*(handle: LoaderHandle, request: Request) =
+  template t(body: untyped) =
+    if not body:
+      return
   if request.url.pathname == "blank":
-    ostream.swrite(0)
-    ostream.swrite(200) # ok
-    let headers = newHeaders(HeaderTable)
-    ostream.swrite(headers)
+    t handle.sendResult(0)
+    t handle.sendStatus(200) # ok
+    t handle.sendHeaders(newHeaders(HeaderTable))
   elif request.url.pathname == "chawan":
-    ostream.swrite(0)
-    ostream.swrite(200) # ok
-    let headers = newHeaders(HeaderTable)
-    ostream.swrite(headers)
-    ostream.write(chawan)
+    t handle.sendResult(0)
+    t handle.sendStatus(200) # ok
+    t handle.sendHeaders(newHeaders(HeaderTable))
+    t handle.sendData(chawan)
   else:
-    ostream.swrite(-1)
-  ostream.flush()
-
+    t handle.sendResult(ERROR_ABOUT_PAGE_NOT_FOUND)
diff --git a/src/io/connecterror.nim b/src/io/connecterror.nim
new file mode 100644
index 00000000..563a4291
--- /dev/null
+++ b/src/io/connecterror.nim
@@ -0,0 +1,17 @@
+import bindings/curl
+
+type ConnectErrorCode* = enum
+  ERROR_ABOUT_PAGE_NOT_FOUND = (-6, "about page not found")
+  ERROR_FILE_NOT_FOUND = (-5, "file not found")
+  ERROR_SOURCE_NOT_FOUND = (-4, "clone source could not be found"),
+  ERROR_LOADER_KILLED = (-3, "loader killed during transfer"),
+  ERROR_DISALLOWED_URL = (-2, "url not allowed by filter"),
+  ERROR_UNKNOWN_SCHEME = (-1, "unknown scheme")
+
+converter toInt*(code: ConnectErrorCode): int =
+  return int(code)
+
+func getLoaderErrorMessage*(code: int): string =
+  if code < 0:
+    return $ConnectErrorCode(code)
+  return $curl_easy_strerror(CURLcode(cint(code)))
diff --git a/src/io/file.nim b/src/io/file.nim
index b9fe0ce2..fe732d6c 100644
--- a/src/io/file.nim
+++ b/src/io/file.nim
@@ -3,18 +3,28 @@ import os
 import streams
 import tables
 
+import io/connecterror
 import io/headers
-import ips/serialize
+import io/loaderhandle
 import types/url
 
-proc loadDir(url: URL, path: string, ostream: Stream) =
-  ostream.swrite(0)
-  ostream.swrite(200) # ok
-  ostream.swrite(newHeaders({"Content-Type": "text/html"}.toTable()))
-  ostream.write("""
+proc loadDir(handle: LoaderHandle, url: URL, path: string) =
+  template t(body: untyped) =
+    if not body:
+      return
+  var path = path
+  if path[^1] != '/': #TODO dos/windows
+    path &= '/'
+  var base = $url
+  if base[^1] != '/': #TODO dos/windows
+    base &= '/'
+  t handle.sendResult(0)
+  t handle.sendStatus(200) # ok
+  t handle.sendHeaders(newHeaders({"Content-Type": "text/html"}.toTable()))
+  t handle.sendData("""
 <HTML>
 <HEAD>
-<BASE HREF="""" & $url & """">
+<BASE HREF="""" & base & """">
 <TITLE>Directory list of """ & path & """</TITLE>
 </HEAD>
 <BODY>
@@ -28,29 +38,31 @@ proc loadDir(url: URL, path: string, ostream: Stream) =
   for (pc, file) in fs:
     case pc
     of pcDir:
-      ostream.write("[DIR]&nbsp; ")
+      t handle.sendData("[DIR]&nbsp; ")
     of pcFile:
-      ostream.write("[FILE] ")
+      t handle.sendData("[FILE] ")
     of pcLinkToDir, pcLinkToFile:
-      ostream.write("[LINK] ")
+      t handle.sendData("[LINK] ")
     var fn = file
     if pc == pcDir:
       fn &= '/'
-    ostream.write("<A HREF=\"" & fn & "\">" & fn & "</A>")
+    t handle.sendData("<A HREF=\"" & fn & "\">" & fn & "</A>")
     if pc in {pcLinkToDir, pcLinkToFile}:
-      ostream.write(" -> " & expandSymlink(path / file))
-    ostream.write("<br>")
-  ostream.write("""
+      discard handle.sendData(" -> " & expandSymlink(path / file))
+    t handle.sendData("<br>")
+  t handle.sendData("""
 </BODY>
 </HTML>""")
-  ostream.flush()
 
-proc loadSymlink(path: string, ostream: Stream) =
-  ostream.swrite(0)
-  ostream.swrite(200) # ok
-  ostream.swrite(newHeaders({"Content-Type": "text/html"}.toTable()))
+proc loadSymlink(handle: LoaderHandle, path: string) =
+  template t(body: untyped) =
+    if not body:
+      return
+  t handle.sendResult(0)
+  t handle.sendStatus(200) # ok
+  t handle.sendHeaders(newHeaders({"Content-Type": "text/html"}.toTable()))
   let sl = expandSymlink(path)
-  ostream.write("""
+  t handle.sendData("""
 <HTML>
 <HEAD>
 <TITLE>Symlink view<TITLE>
@@ -59,10 +71,26 @@ proc loadSymlink(path: string, ostream: Stream) =
 Symbolic link to <A HREF="""" & sl & """">""" & sl & """</A></br>
 </BODY>
 </HTML>""")
-  ostream.flush()
 
+proc loadFile(handle: LoaderHandle, istream: Stream) =
+  template t(body: untyped) =
+    if not body:
+      return
+  t handle.sendResult(0)
+  t handle.sendStatus(200) # ok
+  t handle.sendHeaders(newHeaders())
+  while not istream.atEnd:
+    const bufferSize = 4096
+    var buffer {.noinit.}: array[bufferSize, char]
+    while true:
+      let n = readData(istream, addr buffer[0], bufferSize)
+      if n == 0:
+        break
+      t handle.sendData(addr buffer[0], n)
+      if n < bufferSize:
+        break
 
-proc loadFile*(url: URL, ostream: Stream) =
+proc loadFilePath*(handle: LoaderHandle, url: URL) =
   when defined(windows) or defined(OS2) or defined(DOS):
     let path = url.path.serialize_unicode_dos()
   else:
@@ -70,24 +98,10 @@ proc loadFile*(url: URL, ostream: Stream) =
   let istream = newFileStream(path, fmRead)
   if istream == nil:
     if dirExists(path):
-      loadDir(url, path, ostream)
+      handle.loadDir(url, path)
     elif symlinkExists(path):
-      loadSymlink(path, ostream)
+      handle.loadSymlink(path)
     else:
-      ostream.swrite(-1) # error
-      ostream.flush()
+      discard handle.sendResult(ERROR_FILE_NOT_FOUND)
   else:
-    ostream.swrite(0)
-    ostream.swrite(200) # ok
-    ostream.swrite(newHeaders())
-    while not istream.atEnd:
-      const bufferSize = 4096
-      var buffer {.noinit.}: array[bufferSize, char]
-      while true:
-        let n = readData(istream, addr buffer[0], bufferSize)
-        if n == 0:
-          break
-        ostream.writeData(addr buffer[0], n)
-        ostream.flush()
-        if n < bufferSize:
-          break
+    handle.loadFile(istream)
diff --git a/src/io/http.nim b/src/io/http.nim
index 1ebcaf72..0a5a6d79 100644
--- a/src/io/http.nim
+++ b/src/io/http.nim
@@ -1,11 +1,10 @@
 import options
-import streams
 import strutils
 
 import bindings/curl
 import io/headers
+import io/loaderhandle
 import io/request
-import ips/serialize
 import types/blob
 import types/formdata
 import types/url
@@ -13,26 +12,27 @@ import utils/opt
 import utils/twtstr
 
 type
-  HandleData* = ref HandleDataObj
-  HandleDataObj = object
+  CurlHandle* = ref CurlHandleObj
+  CurlHandleObj = object
     curl*: CURL
     statusline: bool
     headers: Headers
     request: Request
-    ostream*: Stream
+    handle*: LoaderHandle
     mime: curl_mime
     slist: curl_slist
 
-func newHandleData(curl: CURL, request: Request, ostream: Stream): HandleData =
-  let handleData = HandleData(
+func newCurlHandle(curl: CURL, request: Request, handle: LoaderHandle):
+    CurlHandle =
+  return CurlHandle(
     headers: newHeaders(),
     curl: curl,
-    ostream: ostream,
+    handle: handle,
     request: request
   )
-  return handleData
 
-proc cleanup*(handleData: HandleData) =
+proc cleanup*(handleData: CurlHandle) =
+  handleData.handle.close()
   if handleData.mime != nil:
     curl_mime_free(handleData.mime)
   if handleData.slist != nil:
@@ -48,58 +48,51 @@ template setopt(curl: CURL, opt: CURLoption, arg: string) =
 template getinfo(curl: CURL, info: CURLINFO, arg: typed) =
   discard curl_easy_getinfo(curl, info, arg)
 
-proc curlWriteHeader(p: cstring, size: csize_t, nitems: csize_t, userdata: pointer): csize_t {.cdecl.} =
+proc curlWriteHeader(p: cstring, size: csize_t, nitems: csize_t,
+    userdata: pointer): csize_t {.cdecl.} =
   var line = newString(nitems)
   for i in 0..<nitems:
     line[i] = p[i]
 
-  let op = cast[HandleData](userdata)
+  let op = cast[CurlHandle](userdata)
   if not op.statusline:
     op.statusline = true
-    try:
-      op.ostream.swrite(int(CURLE_OK))
-    except IOError: # Broken pipe
+    if not op.handle.sendResult(int(CURLE_OK)):
       return 0
     var status: clong
     op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status)
-    op.ostream.swrite(cast[int](status))
+    if not op.handle.sendStatus(cast[int](status)):
+      return 0
     return nitems
 
   let k = line.until(':')
 
   if k.len == line.len:
     # empty line (last, before body) or invalid (=> error)
-    op.ostream.swrite(op.headers)
+    if not op.handle.sendHeaders(op.headers):
+      return 0
     return nitems
 
   let v = line.substr(k.len + 1).strip()
   op.headers.add(k, v)
   return nitems
 
-proc curlWriteBody(p: cstring, size: csize_t, nmemb: csize_t, userdata: pointer): csize_t {.cdecl.} =
-  let handleData = cast[HandleData](userdata)
+# From the documentation: size is always 1.
+proc curlWriteBody(p: cstring, size: csize_t, nmemb: csize_t,
+    userdata: pointer): csize_t {.cdecl.} =
+  let handleData = cast[CurlHandle](userdata)
   if nmemb > 0:
-    try:
-      handleData.ostream.writeData(p, int(nmemb))
-    except IOError: # Broken pipe
+    if not handleData.handle.sendData(p, int(nmemb)):
       return 0
   return nmemb
 
-proc applyPostBody(curl: CURL, request: Request, handleData: HandleData) =
+proc applyPostBody(curl: CURL, request: Request, handleData: CurlHandle) =
   if request.multipart.isOk:
     handleData.mime = curl_mime_init(curl)
-    if handleData.mime == nil:
-      # fail (TODO: raise?)
-      handleData.ostream.swrite(-1)
-      handleData.ostream.flush()
-      return
+    doAssert handleData.mime != nil
     for entry in request.multipart.get:
       let part = curl_mime_addpart(handleData.mime)
-      if part == nil:
-        # fail (TODO: raise?)
-        handleData.ostream.swrite(-1)
-        handleData.ostream.flush()
-        return
+      doAssert part != nil
       curl_mime_name(part, cstring(entry.name))
       if entry.isstr:
         curl_mime_data(part, cstring(entry.svalue), csize_t(entry.svalue.len))
@@ -116,15 +109,13 @@ proc applyPostBody(curl: CURL, request: Request, handleData: HandleData) =
     curl.setopt(CURLOPT_POSTFIELDS, cstring(request.body.get))
     curl.setopt(CURLOPT_POSTFIELDSIZE, request.body.get.len)
 
-proc loadHttp*(curlm: CURLM, request: Request, ostream: Stream): HandleData =
+proc loadHttp*(handle: LoaderHandle, curlm: CURLM,
+    request: Request): CurlHandle =
   let curl = curl_easy_init()
-  if curl == nil:
-    ostream.swrite(-1)
-    ostream.flush()
-    return # fail
+  doAssert curl != nil
   let surl = request.url.serialize()
   curl.setopt(CURLOPT_URL, surl)
-  let handleData = curl.newHandleData(request, ostream)
+  let handleData = curl.newCurlHandle(request, handle)
   curl.setopt(CURLOPT_WRITEDATA, handleData)
   curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody)
   curl.setopt(CURLOPT_HEADERDATA, handleData)
@@ -146,8 +137,6 @@ proc loadHttp*(curlm: CURLM, request: Request, ostream: Stream): HandleData =
     curl.setopt(CURLOPT_HTTPHEADER, handleData.slist)
   let res = curl_multi_add_handle(curlm, curl)
   if res != CURLM_OK:
-    ostream.swrite(int(res))
-    ostream.flush()
-    #TODO: raise here?
-    return
+    discard handle.sendResult(int(res))
+    return nil
   return handleData
diff --git a/src/io/loader.nim b/src/io/loader.nim
index 46f694e6..8e125b31 100644
--- a/src/io/loader.nim
+++ b/src/io/loader.nim
@@ -12,18 +12,21 @@
 # The body is passed to the stream as-is, so effectively nothing can follow it.
 
 import nativesockets
+import net
 import options
+import posix
 import streams
+import strutils
 import tables
-import net
-when defined(posix):
-  import posix
 
 import bindings/curl
+import data/charset
 import io/about
+import io/connecterror
 import io/file
 import io/headers
 import io/http
+import io/loaderhandle
 import io/posixstream
 import io/promise
 import io/request
@@ -34,9 +37,9 @@ import ips/serversocket
 import ips/socketstream
 import js/javascript
 import types/cookie
-import types/mime
 import types/referer
 import types/url
+import utils/mimeguess
 import utils/twtstr
 
 export request
@@ -62,14 +65,9 @@ type
     response: Response
     bodyRead: Promise[string]
 
-  ConnectErrorCode* = enum
-    ERROR_SOURCE_NOT_FOUND = (-4, "clone source could not be found"),
-    ERROR_LOADER_KILLED = (-3, "loader killed during transfer"),
-    ERROR_DISALLOWED_URL = (-2, "url not allowed by filter"),
-    ERROR_UNKNOWN_SCHEME = (-1, "unknown scheme")
-
   LoaderCommand = enum
-    LOAD, QUIT
+    LOAD
+    QUIT
 
   LoaderContext = ref object
     ssock: ServerSocket
@@ -77,7 +75,7 @@ type
     curlm: CURLM
     config: LoaderConfig
     extra_fds: seq[curl_waitfd]
-    handleList: seq[HandleData]
+    handleList: seq[CurlHandle]
 
   LoaderConfig* = object
     defaultheaders*: Headers
@@ -91,39 +89,36 @@ type
 
   FetchPromise* = Promise[Result[Response, JSError]]
 
-converter toInt*(code: ConnectErrorCode): int =
-  return int(code)
-
 proc addFd(ctx: LoaderContext, fd: int, flags: int) =
   ctx.extra_fds.add(curl_waitfd(
     fd: cast[cint](fd),
     events: cast[cshort](flags)
   ))
 
-proc loadResource(ctx: LoaderContext, request: Request, ostream: Stream) =
+proc loadResource(ctx: LoaderContext, request: Request, handle: LoaderHandle) =
   case request.url.scheme
   of "file":
-    loadFile(request.url, ostream)
-    ostream.close()
+    handle.loadFilePath(request.url)
+    handle.close()
   of "http", "https":
-    let handleData = loadHttp(ctx.curlm, request, ostream)
+    let handleData = handle.loadHttp(ctx.curlm, request)
     if handleData != nil:
       ctx.handleList.add(handleData)
   of "about":
-    loadAbout(request, ostream)
-    ostream.close()
+    handle.loadAbout(request)
+    handle.close()
   else:
-    ostream.swrite(ERROR_UNKNOWN_SCHEME) # error
-    ostream.close()
+    discard handle.sendResult(ERROR_UNKNOWN_SCHEME)
+    handle.close()
 
 proc onLoad(ctx: LoaderContext, stream: Stream) =
   var request: Request
   stream.sread(request)
   if not ctx.config.filter.match(request.url):
-    stream.swrite(ERROR_DISALLOWED_URL) # error
-    stream.flush()
+    stream.swrite(ERROR_DISALLOWED_URL)
     stream.close()
   else:
+    let handle = newLoaderHandle(stream, request.canredir)
     for k, v in ctx.config.defaultHeaders.table:
       if k notin request.headers.table:
         request.headers.table[k] = v
@@ -138,7 +133,7 @@ proc onLoad(ctx: LoaderContext, stream: Stream) =
         request.headers["Referer"] = r
     if request.proxy == nil or not ctx.config.acceptProxy:
       request.proxy = ctx.config.proxy
-    ctx.loadResource(request, stream)
+    ctx.loadResource(request, handle)
 
 proc acceptConnection(ctx: LoaderContext) =
   #TODO TODO TODO acceptSocketStream should be non-blocking here,
@@ -160,15 +155,10 @@ proc acceptConnection(ctx: LoaderContext) =
     # (TODO: this is probably not a very good idea.)
     stream.close()
 
-proc finishCurlTransfer(ctx: LoaderContext, handleData: HandleData, res: int) =
+proc finishCurlTransfer(ctx: LoaderContext, handleData: CurlHandle, res: int) =
   if res != int(CURLE_OK):
-    try:
-      handleData.ostream.swrite(int(res))
-      handleData.ostream.flush()
-    except IOError: # Broken pipe
-      discard
+    discard handleData.handle.sendResult(int(res))
   discard curl_multi_remove_handle(ctx.curlm, handleData.curl)
-  handleData.ostream.close()
   handleData.cleanup()
 
 proc exitLoader(ctx: LoaderContext) =
@@ -192,7 +182,9 @@ proc initLoaderContext(fd: cint, config: LoaderConfig): LoaderContext =
     config: config
   )
   gctx = ctx
-  ctx.ssock = initServerSocket()
+  #TODO ideally, buffered would be true. Unfortunately this conflicts with
+  # sendFileHandle/recvFileHandle.
+  ctx.ssock = initServerSocket(buffered = false)
   # The server has been initialized, so the main process can resume execution.
   var writef: File
   if not open(writef, FileHandle(fd), fmWrite):
@@ -235,11 +227,39 @@ proc runFileLoader*(fd: cint, config: LoaderConfig) =
         ctx.handleList.del(idx)
   ctx.exitLoader()
 
-proc applyHeaders(request: Request, response: Response) =
+proc getAttribute(contentType, attrname: string): string =
+  let kvs = contentType.after(';')
+  var i = kvs.find(attrname)
+  var s = ""
+  if i != -1 and kvs.len > i + attrname.len and
+      kvs[i + attrname.len] == '=':
+    i += attrname.len + 1
+    while i < kvs.len and kvs[i] in AsciiWhitespace:
+      inc i
+    var q = false
+    for j in i ..< kvs.len:
+      if q:
+        s &= kvs[j]
+      else:
+        if kvs[j] == '\\':
+          q = true
+        elif kvs[j] == ';' or kvs[j] in AsciiWhitespace:
+          break
+        else:
+          s &= kvs[j]
+  return s
+
+proc applyHeaders(loader: FileLoader, request: Request, response: Response) =
   if "Content-Type" in response.headers.table:
-    response.contenttype = response.headers.table["Content-Type"][0].until(';')
+    #TODO this is inefficient and broken on several levels. (In particular,
+    # it breaks mailcap named attributes other than charset.)
+    # Ideally, contentType would be a separate object type.
+    let header = response.headers.table["Content-Type"][0].toLowerAscii()
+    response.contenttype = header.until(';').strip().toLowerAscii()
+    response.charset = getCharset(header.getAttribute("charset"))
   else:
-    response.contenttype = guessContentType($response.url.path)
+    response.contenttype = guessContentType($response.url.path,
+      "application/octet-stream", DefaultGuess)
   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]
@@ -276,6 +296,18 @@ proc fetch*(loader: FileLoader, input: Request): FetchPromise =
 
 const BufferSize = 4096
 
+proc handleHeaders(loader: FileLoader, request: Request, response: Response,
+    stream: Stream): bool =
+  var status: int
+  stream.sread(status)
+  response.status = cast[uint16](status)
+  response.headers = newHeaders()
+  stream.sread(response.headers)
+  loader.applyHeaders(request, response)
+  # Only a stream of the response body may arrive after this point.
+  response.body = stream
+  return true # success
+
 proc onConnected*(loader: FileLoader, fd: int) =
   let connectData = loader.connecting[fd]
   let stream = connectData.stream
@@ -283,8 +315,8 @@ proc onConnected*(loader: FileLoader, fd: int) =
   let request = connectData.request
   var res: int
   stream.sread(res)
-  if res == 0:
-    let response = newResponse(res, request, fd, stream)
+  let response = newResponse(res, request, fd, stream)
+  if res == 0 and loader.handleHeaders(request, response, stream):
     assert loader.unregisterFun != nil
     let realCloseImpl = stream.closeImpl
     stream.closeImpl = nil
@@ -293,12 +325,6 @@ proc onConnected*(loader: FileLoader, fd: int) =
       loader.unregistered.add(fd)
       loader.unregisterFun(fd)
       realCloseImpl(stream)
-    var status: int
-    stream.sread(status)
-    response.status = cast[uint16](status)
-    stream.sread(response.headers)
-    applyHeaders(request, response)
-    response.body = stream
     loader.ongoing[fd] = OngoingData(
       response: response,
       readbufsize: BufferSize,
@@ -339,31 +365,23 @@ proc onRead*(loader: FileLoader, fd: int) =
 proc onError*(loader: FileLoader, fd: int) =
   loader.onRead(fd)
 
-proc doRequest*(loader: FileLoader, request: Request, blocking = true): Response =
-  new(result)
-  result.url = request.url
+proc doRequest*(loader: FileLoader, request: Request, blocking = true,
+    canredir = false): Response =
+  let response = Response(url: request.url)
   let stream = connectSocketStream(loader.process, false, blocking = true)
+  if canredir:
+    request.canredir = true #TODO set this somewhere else?
   stream.swrite(LOAD)
   stream.swrite(request)
   stream.flush()
-  stream.sread(result.res)
-  if result.res == 0:
-    var status: int
-    stream.sread(status)
-    result.status = cast[uint16](status)
-    stream.sread(result.headers)
-    applyHeaders(request, result)
-    # Only a stream of the response body may arrive after this point.
-    result.body = stream
-    if not blocking:
-      stream.source.getFd().setBlocking(blocking)
+  stream.sread(response.res)
+  if response.res == 0:
+    if loader.handleHeaders(request, response, stream):
+      if not blocking:
+        stream.source.getFd().setBlocking(blocking)
+  return response
 
 proc quit*(loader: FileLoader) =
   let stream = connectSocketStream(loader.process)
   if stream != nil:
     stream.swrite(QUIT)
-
-func getLoaderErrorMessage*(code: int): string =
-  if code < 0:
-    return $ConnectErrorCode(code)
-  return $curl_easy_strerror(CURLcode(cint(code)))
diff --git a/src/io/loaderhandle.nim b/src/io/loaderhandle.nim
new file mode 100644
index 00000000..077b1a2a
--- /dev/null
+++ b/src/io/loaderhandle.nim
@@ -0,0 +1,73 @@
+import net
+import streams
+
+import io/posixstream
+import io/headers
+import ips/serialize
+import ips/socketstream
+
+type LoaderHandle* = ref object
+  ostream: Stream
+  # Only the first handle can be redirected, because a) mailcap can only
+  # redirect the first handle and b) async redirects would result in race
+  # conditions that would be difficult to untangle.
+  canredir: bool
+  sostream: Stream # saved ostream when redirected
+
+# Create a new loader handle, with the output stream ostream.
+proc newLoaderHandle*(ostream: Stream, canredir: bool): LoaderHandle =
+  return LoaderHandle(ostream: ostream, canredir: canredir)
+
+proc getFd*(handle: LoaderHandle): int =
+  return int(SocketStream(handle.ostream).source.getFd())
+
+proc sendResult*(handle: LoaderHandle, res: int): bool =
+  try:
+    handle.ostream.swrite(res)
+    return true
+  except IOError: # broken pipe
+    return false
+
+proc sendStatus*(handle: LoaderHandle, status: int): bool =
+  try:
+    handle.ostream.swrite(status)
+    return true
+  except IOError: # broken pipe
+    return false
+
+proc sendHeaders*(handle: LoaderHandle, headers: Headers): bool =
+  try:
+    handle.ostream.swrite(headers)
+    if handle.canredir:
+      var redir: bool
+      handle.ostream.sread(redir)
+      if redir:
+        let fd = SocketStream(handle.ostream).recvFileHandle()
+        handle.sostream = handle.ostream
+        let stream = newPosixStream(fd)
+        handle.ostream = stream
+    return true
+  except IOError: # broken pipe
+    return false
+
+proc sendData*(handle: LoaderHandle, p: pointer, nmemb: int): bool =
+  try:
+    handle.ostream.writeData(p, nmemb)
+    return true
+  except IOError: # broken pipe
+    return false
+
+proc sendData*(handle: LoaderHandle, s: string): bool =
+  if s.len > 0:
+    return handle.sendData(unsafeAddr s[0], s.len)
+  return true
+
+proc close*(handle: LoaderHandle) =
+  if handle.sostream != nil:
+    try:
+      handle.sostream.swrite(true)
+    except IOError:
+      # ignore error, that just means the buffer has already closed the stream
+      discard
+    handle.sostream.close()
+  handle.ostream.close()
diff --git a/src/io/posixstream.nim b/src/io/posixstream.nim
index 10fd2237..e24facde 100644
--- a/src/io/posixstream.nim
+++ b/src/io/posixstream.nim
@@ -30,6 +30,10 @@ proc raisePosixIOError*() =
   else:
     raise newException(IOError, $strerror(errno))
 
+proc psClose(s: Stream) =
+  let s = cast[PosixStream](s)
+  discard close(s.fd)
+
 proc psReadData(s: Stream, buffer: pointer, len: int): int =
   assert len != 0
   let s = cast[PosixStream](s)
@@ -63,6 +67,7 @@ proc psAtEnd(s: Stream): bool =
 proc newPosixStream*(fd: FileHandle): PosixStream =
   return PosixStream(
     fd: fd,
+    closeImpl: psClose,
     readDataImpl: psReadData,
     writeDataImpl: psWriteData,
     atEndImpl: psAtEnd
diff --git a/src/io/request.nim b/src/io/request.nim
index 4ddd5d6d..f609360b 100644
--- a/src/io/request.nim
+++ b/src/io/request.nim
@@ -73,6 +73,7 @@ type
     destination* {.jsget.}: RequestDestination
     credentialsMode* {.jsget.}: CredentialsMode
     proxy*: URL #TODO do something with this
+    canredir*: bool
  
   ReadableStream* = ref object of Stream
     isource*: Stream
@@ -154,7 +155,8 @@ proc newReadableStream*(isource: Stream): ReadableStream =
 func newRequest*(url: URL, httpmethod = HTTP_GET, headers = newHeaders(),
     body = opt(string), multipart = opt(FormData), mode = RequestMode.NO_CORS,
     credentialsMode = CredentialsMode.SAME_ORIGIN,
-    destination = RequestDestination.NO_DESTINATION, proxy: URL = nil): Request =
+    destination = RequestDestination.NO_DESTINATION, proxy: URL = nil,
+    canredir = false): Request =
   return Request(
     url: url,
     httpmethod: httpmethod,
@@ -169,7 +171,8 @@ func newRequest*(url: URL, httpmethod = HTTP_GET, headers = newHeaders(),
 
 func newRequest*(url: URL, httpmethod = HTTP_GET,
     headers: seq[(string, string)] = @[], body = opt(string),
-    multipart = opt(FormData), mode = RequestMode.NO_CORS, proxy: URL = nil):
+    multipart = opt(FormData), mode = RequestMode.NO_CORS, proxy: URL = nil,
+    canredir = false):
     Request =
   let hl = newHeaders()
   for pair in headers:
diff --git a/src/io/response.nim b/src/io/response.nim
index b64f1504..dedddbcd 100644
--- a/src/io/response.nim
+++ b/src/io/response.nim
@@ -1,6 +1,7 @@
 import streams
 
 import bindings/quickjs
+import data/charset
 import io/headers
 import io/promise
 import io/request
@@ -20,6 +21,7 @@ type
     url*: URL #TODO should be urllist?
     unregisterFun*: proc()
     bodyRead*: Promise[string]
+    charset*: Charset
 
 jsDestructor(Response)
 
diff --git a/src/io/tempfile.nim b/src/io/tempfile.nim
new file mode 100644
index 00000000..d99ea4dc
--- /dev/null
+++ b/src/io/tempfile.nim
@@ -0,0 +1,18 @@
+import os
+
+var tmpf_seq: int
+proc getTempFile*(tmpdir: string, ext = ""): string =
+  if not dirExists(tmpdir):
+    createDir(tmpdir)
+  var tmpf = tmpdir / "chatmp" & $tmpf_seq
+  if ext != "":
+    tmpf &= "."
+    tmpf &= ext
+  while fileExists(tmpf):
+    inc tmpf_seq
+    tmpf = tmpdir / "chatmp" & $tmpf_seq
+    if ext != "":
+      tmpf &= "."
+      tmpf &= ext
+  inc tmpf_seq
+  return tmpf