about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bindings/curl.nim3
-rw-r--r--src/css/cssparser.nim2
-rw-r--r--src/loader/cgi.nim32
-rw-r--r--src/loader/connecterror.nim37
-rw-r--r--src/loader/curlhandle.nim32
-rw-r--r--src/loader/curlwrap.nim10
-rw-r--r--src/loader/dirlist.nim60
-rw-r--r--src/loader/http.nim137
-rw-r--r--src/loader/loader.nim130
-rw-r--r--src/loader/loaderhandle.nim4
-rw-r--r--src/server/buffer.nim12
-rw-r--r--src/types/blob.nim13
-rw-r--r--src/types/formdata.nim66
-rw-r--r--src/xhr/formdata.nim15
14 files changed, 195 insertions, 358 deletions
diff --git a/src/bindings/curl.nim b/src/bindings/curl.nim
index cd9e409a..a36a3fb7 100644
--- a/src/bindings/curl.nim
+++ b/src/bindings/curl.nim
@@ -95,15 +95,18 @@ type
     CURLOPT_HEADERDATA = CURLOPTTYPE_CBPOINT + 29
     CURLOPT_ACCEPT_ENCODING = CURLOPTTYPE_STRINGPOINT + 102
     CURLOPT_MIMEPOST = CURLOPTTYPE_OBJECTPOINT + 269
+    CURLOPT_PREREQDATA = CURLOPTTYPE_CBPOINT + 313
 
     # Functionpoint
     CURLOPT_WRITEFUNCTION = CURLOPTTYPE_FUNCTIONPOINT + 11
     CURLOPT_READFUNCTION = CURLOPTTYPE_FUNCTIONPOINT + 12
     CURLOPT_HEADERFUNCTION = CURLOPTTYPE_FUNCTIONPOINT + 79
+    CURLOPT_PREREQFUNCTION = CURLOPTTYPE_FUNCTIONPOINT + 312
 
     # Off-t
     CURLOPT_INFILESIZE_LARGE = CURLOPTTYPE_OFF_T + 115
     CURLOPT_RESUME_FROM_LARGE = CURLOPTTYPE_OFF_T + 116
+    CURLOPT_POSTFIELDSIZE_LARGE = CURLOPTTYPE_OFF_T + 120
 
     # Blob
     CURLOPT_SSLCERT_BLOB = CURLOPTTYPE_BLOB + 291
diff --git a/src/css/cssparser.nim b/src/css/cssparser.nim
index a8081ecc..a825e94d 100644
--- a/src/css/cssparser.nim
+++ b/src/css/cssparser.nim
@@ -248,7 +248,7 @@ proc consumeEscape(state: var CSSTokenizerState): string =
       num *= 0x10
       num += hexValue(c)
       inc i
-    if state.peek() in AsciiWhitespace:
+    if state.has() and state.peek() in AsciiWhitespace:
       discard state.consume()
     if num == 0 or num > 0x10FFFF or num in 0xD800..0xDFFF:
       return $Rune(0xFFFD)
diff --git a/src/loader/cgi.nim b/src/loader/cgi.nim
index 89361a7c..8fe96274 100644
--- a/src/loader/cgi.nim
+++ b/src/loader/cgi.nim
@@ -10,6 +10,7 @@ import loader/connecterror
 import loader/headers
 import loader/loaderhandle
 import loader/request
+import types/formdata
 import types/opt
 import types/url
 import utils/twtstr
@@ -37,6 +38,10 @@ proc setupEnv(cmd, scriptName, pathInfo, requestURI: string, request: Request,
   putEnv("SCRIPT_FILENAME", cmd)
   putEnv("REQUEST_URI", requestURI)
   putEnv("REQUEST_METHOD", $request.httpmethod)
+  var headers = ""
+  for k, v in request.headers:
+    headers &= k & ": " & v & "\r\n"
+  putEnv("REQUEST_HEADERS", headers)
   if prevURL != nil:
     putMappedURL(prevURL)
   if pathInfo != "":
@@ -44,19 +49,17 @@ proc setupEnv(cmd, scriptName, pathInfo, requestURI: string, request: Request,
   if url.query.isSome:
     putEnv("QUERY_STRING", url.query.get)
   if request.httpmethod == HTTP_POST:
-    putEnv("CONTENT_TYPE", request.headers.getOrDefault("Content-Type", ""))
+    if request.multipart.isSome:
+      putEnv("CONTENT_TYPE", request.multipart.get.getContentType())
+    else:
+      putEnv("CONTENT_TYPE", request.headers.getOrDefault("Content-Type", ""))
     putEnv("CONTENT_LENGTH", $contentLen)
   if "Cookie" in request.headers:
     putEnv("HTTP_COOKIE", request.headers["Cookie"])
   if request.referer != nil:
     putEnv("HTTP_REFERER", $request.referer)
   if request.proxy != nil:
-    let s = $request.proxy
-    if request.proxy.scheme == "https" or request.proxy.scheme == "http":
-      putEnv("http_proxy", s)
-      putEnv("HTTP_PROXY", s)
-      putEnv("HTTPS_proxy", s)
-    putEnv("ALL_PROXY", s)
+    putEnv("ALL_PROXY", $request.proxy)
 
 type ControlResult = enum
   RESULT_CONTROL_DONE, RESULT_CONTROL_CONTINUE, RESULT_ERROR
@@ -184,9 +187,7 @@ proc loadCGI*(handle: LoaderHandle, request: Request, cgiDir: seq[string],
   if request.body.isSome:
     contentLen = request.body.get.len
   elif request.multipart.isSome:
-    #TODO multipart
-    # maybe use curl formdata? (the mime api has no serialization functions)
-    discard
+    contentLen = request.multipart.get.calcLength()
   let pid = fork()
   if pid == -1:
     t handle.sendResult(ERROR_FAIL_SETUP_CGI)
@@ -214,8 +215,9 @@ proc loadCGI*(handle: LoaderHandle, request: Request, cgiDir: seq[string],
       if request.body.isSome:
         ps.write(request.body.get)
       elif request.multipart.isSome:
-        #TODO
-        discard
+        let multipart = request.multipart.get
+        for entry in multipart.entries:
+          ps.writeEntry(entry, multipart.boundary)
       ps.close()
     let ps = newPosixStream(pipefd[0])
     let headers = newHeaders()
@@ -249,8 +251,4 @@ proc loadCGI*(handle: LoaderHandle, request: Request, cgiDir: seq[string],
           handle.handleLine(line, headers)
     t handle.sendStatus(status)
     t handle.sendHeaders(headers)
-    var buffer: array[4096, uint8]
-    while not ps.atEnd:
-      let n = ps.readData(addr buffer[0], buffer.len)
-      t handle.sendData(addr buffer[0], n)
-    ps.close()
+    handle.istream = ps
diff --git a/src/loader/connecterror.nim b/src/loader/connecterror.nim
index 913e007f..acd4a28f 100644
--- a/src/loader/connecterror.nim
+++ b/src/loader/connecterror.nim
@@ -1,28 +1,31 @@
-import bindings/curl
-
 type ConnectErrorCode* = enum
-  ERROR_CGI_NO_DATA = (-17, "CGI script returned no data")
-  ERROR_CGI_MALFORMED_HEADER = (-16, "CGI script returned a malformed header")
-  ERROR_CGI_INVALID_CHA_CONTROL = (-15, "CGI got invalid Cha-Control header")
-  ERROR_TOO_MANY_REWRITES = (-14, "too many URI method map rewrites")
-  ERROR_INVALID_URI_METHOD_ENTRY = (-13, "invalid URI method entry")
-  ERROR_CGI_FILE_NOT_FOUND = (-12, "CGI file not found")
-  ERROR_INVALID_CGI_PATH = (-11, "invalid CGI path")
-  ERROR_FAIL_SETUP_CGI = (-10, "failed to set up CGI script")
-  ERROR_NO_CGI_DIR = (-9, "no local-CGI directory configured")
-  ERROR_INVALID_METHOD = (-8, "invalid method")
-  ERROR_INVALID_URL = (-7, "invalid URL")
-  ERROR_CONNECTION_REFUSED = (-6, "connection refused")
-  ERROR_FILE_NOT_FOUND = (-5, "file not found")
+  ERROR_CGI_NO_DATA = (-13, "CGI script returned no data")
+  ERROR_CGI_MALFORMED_HEADER = (-12, "CGI script returned a malformed header")
+  ERROR_CGI_INVALID_CHA_CONTROL = (-11, "CGI got invalid Cha-Control header")
+  ERROR_TOO_MANY_REWRITES = (-10, "too many URI method map rewrites")
+  ERROR_INVALID_URI_METHOD_ENTRY = (-9, "invalid URI method entry")
+  ERROR_CGI_FILE_NOT_FOUND = (-8, "CGI file not found")
+  ERROR_INVALID_CGI_PATH = (-7, "invalid CGI path")
+  ERROR_FAIL_SETUP_CGI = (-6, "failed to set up CGI script")
+  ERROR_NO_CGI_DIR = (-5, "no local-CGI directory configured")
   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")
+  CONNECTION_SUCCESS = (0, "connection successful")
+  ERROR_INTERNAL = (1, "internal error")
+  ERROR_INVALID_METHOD = (2, "invalid method")
+  ERROR_INVALID_URL = (3, "invalid URL")
+  ERROR_FILE_NOT_FOUND = (4, "file not found")
+  ERROR_CONNECTION_REFUSED = (5, "connection refused")
+  ERROR_PROXY_REFUSED_TO_CONNECT = (6, "proxy refused to connect")
+  ERROR_FAILED_TO_RESOLVE_HOST = (7, "failed to resolve host")
+  ERROR_FAILED_TO_RESOLVE_PROXY = (8, "failed to resolve proxy")
 
 converter toInt*(code: ConnectErrorCode): int =
   return int(code)
 
 func getLoaderErrorMessage*(code: int): string =
-  if code < 0:
+  if code in int(ConnectErrorCode.low)..int(ConnectErrorCode.high):
     return $ConnectErrorCode(code)
-  return $curl_easy_strerror(CURLcode(cint(code)))
+  return "unexpected error code " & $code
diff --git a/src/loader/curlhandle.nim b/src/loader/curlhandle.nim
deleted file mode 100644
index 3c69c6c0..00000000
--- a/src/loader/curlhandle.nim
+++ /dev/null
@@ -1,32 +0,0 @@
-import bindings/curl
-import loader/headers
-import loader/loaderhandle
-import loader/request
-
-type
-  CurlHandle* = ref object of RootObj
-    curl*: CURL
-    statusline*: bool
-    headers*: Headers
-    request*: Request
-    handle*: LoaderHandle
-    mime*: curl_mime
-    slist*: curl_slist
-    finish*: proc(handle: CurlHandle)
-
-func newCurlHandle*(curl: CURL, request: Request, handle: LoaderHandle):
-    CurlHandle =
-  return CurlHandle(
-    headers: newHeaders(),
-    curl: curl,
-    handle: handle,
-    request: request
-  )
-
-proc cleanup*(handleData: CurlHandle) =
-  handleData.handle.close()
-  if handleData.mime != nil:
-    curl_mime_free(handleData.mime)
-  if handleData.slist != nil:
-    curl_slist_free_all(handleData.slist)
-  curl_easy_cleanup(handleData.curl)
diff --git a/src/loader/curlwrap.nim b/src/loader/curlwrap.nim
deleted file mode 100644
index 7aef4182..00000000
--- a/src/loader/curlwrap.nim
+++ /dev/null
@@ -1,10 +0,0 @@
-import bindings/curl
-
-template setopt*(curl: CURL, opt: CURLoption, arg: typed) =
-  discard curl_easy_setopt(curl, opt, arg)
-
-template setopt*(curl: CURL, opt: CURLoption, arg: string) =
-  discard curl_easy_setopt(curl, opt, cstring(arg))
-
-template getinfo*(curl: CURL, info: CURLINFO, arg: typed) =
-  discard curl_easy_getinfo(curl, info, arg)
diff --git a/src/loader/dirlist.nim b/src/loader/dirlist.nim
deleted file mode 100644
index 2328cd55..00000000
--- a/src/loader/dirlist.nim
+++ /dev/null
@@ -1,60 +0,0 @@
-import algorithm
-
-import utils/twtstr
-
-type DirlistItemType = enum
-  ITEM_FILE, ITEM_LINK, ITEM_DIR
-
-type DirlistItem* = object
-  name*: string
-  modified*: string
-  case t*: DirlistItemType
-  of ITEM_LINK:
-    linkto*: string
-  of ITEM_FILE:
-    nsize*: int
-  of ITEM_DIR:
-    discard
-
-type NameWidthTuple = tuple[name: string, width: int]
-
-func makeDirlist*(items: seq[DirlistItem]): string =
-  var names: seq[NameWidthTuple]
-  var maxw = 20
-  for item in items:
-    var name = item.name
-    if item.t == ITEM_LINK:
-      name &= '@'
-    elif item.t == ITEM_DIR:
-      name &= '/'
-    let w = name.width()
-    maxw = max(w, maxw)
-    names.add((name, w))
-  names.sort(proc(a, b: NameWidthTuple): int = cmp(a.name, b.name))
-  var outs = "<A HREF=\"../\">[Upper Directory]</A>\n"
-  for i in 0 ..< items.len:
-    let item = items[i]
-    var (name, width) = names[i]
-    var path = percentEncode(item.name, PathPercentEncodeSet)
-    if item.t == ITEM_LINK:
-      if item.linkto.len > 0 and item.linkto[^1] == '/':
-        # If the target is a directory, treat it as a directory. (For FTP.)
-        path &= '/'
-    elif item.t == ITEM_DIR:
-      path &= '/'
-    var line = "<A HREF=\"" & path & "\">" & htmlEscape(name) & "</A>"
-    while width <= maxw:
-      if width mod 2 == 0:
-        line &= ' '
-      else:
-        line &= '.'
-      inc width
-    if line[^1] != ' ':
-      line &= ' '
-    line &= htmlEscape(item.modified)
-    if item.t == ITEM_FILE:
-      line &= ' ' & convert_size(item.nsize)
-    elif item.t == ITEM_LINK:
-      line &= " -> " & htmlEscape(item.linkto)
-    outs &= line & '\n'
-  return outs
diff --git a/src/loader/http.nim b/src/loader/http.nim
deleted file mode 100644
index d7bc3a8f..00000000
--- a/src/loader/http.nim
+++ /dev/null
@@ -1,137 +0,0 @@
-import options
-import strutils
-
-import bindings/curl
-import loader/curlhandle
-import loader/curlwrap
-import loader/headers
-import loader/loaderhandle
-import loader/request
-import types/blob
-import types/formdata
-import types/opt
-import types/url
-import utils/twtstr
-
-type
-  EarlyHintState = enum
-    NO_EARLY_HINT, EARLY_HINT_STARTED, EARLY_HINT_DONE
-
-  HttpHandle = ref object of CurlHandle
-    earlyhint: EarlyHintState
-
-func newHttpHandle(curl: CURL, request: Request, handle: LoaderHandle):
-    HttpHandle =
-  return HttpHandle(
-    headers: newHeaders(),
-    curl: curl,
-    handle: handle,
-    request: request
-  )
-
-proc curlWriteHeader(p: cstring, size: csize_t, nitems: csize_t,
-    userdata: pointer): csize_t {.cdecl.} =
-  var line = newString(nitems)
-  if nitems > 0:
-    prepareMutation(line)
-    copyMem(addr line[0], p, nitems)
-
-  let op = cast[HttpHandle](userdata)
-  if not op.statusline:
-    op.statusline = true
-    if op.earlyhint == NO_EARLY_HINT:
-      if not op.handle.sendResult(int(CURLE_OK)):
-        return 0
-    var status: clong
-    op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status)
-    if status == 103 and op.earlyhint == NO_EARLY_HINT:
-      op.earlyhint = EARLY_HINT_STARTED
-    else:
-      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)
-    if op.earlyhint == EARLY_HINT_STARTED:
-      # ignore; we do not have a way to stream headers yet.
-      op.earlyhint = EARLY_HINT_DONE
-      # reset statusline; we are awaiting the next line.
-      op.statusline = false
-      return nitems
-    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
-
-# 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[HttpHandle](userdata)
-  if nmemb > 0:
-    if not handleData.handle.sendData(p, int(nmemb)):
-      return 0
-  return nmemb
-
-proc applyPostBody(curl: CURL, request: Request, handleData: HttpHandle) =
-  if request.multipart.isOk:
-    handleData.mime = curl_mime_init(curl)
-    doAssert handleData.mime != nil
-    for entry in request.multipart.get:
-      let part = curl_mime_addpart(handleData.mime)
-      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))
-      else:
-        let blob = entry.value
-        if blob.isfile: #TODO ?
-          curl_mime_filedata(part, cstring(WebFile(blob).path))
-        else:
-          curl_mime_data(part, blob.buffer, csize_t(blob.size))
-        # may be overridden by curl_mime_filedata, so set it here
-        curl_mime_filename(part, cstring(entry.filename))
-    curl.setopt(CURLOPT_MIMEPOST, handleData.mime)
-  elif request.body.issome:
-    curl.setopt(CURLOPT_POSTFIELDS, cstring(request.body.get))
-    curl.setopt(CURLOPT_POSTFIELDSIZE, request.body.get.len)
-
-proc loadHttp*(handle: LoaderHandle, curlm: CURLM,
-    request: Request): HttpHandle =
-  let curl = curl_easy_init()
-  doAssert curl != nil
-  let surl = request.url.serialize()
-  curl.setopt(CURLOPT_URL, surl)
-  let handleData = curl.newHttpHandle(request, handle)
-  curl.setopt(CURLOPT_WRITEDATA, handleData)
-  curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody)
-  curl.setopt(CURLOPT_HEADERDATA, handleData)
-  curl.setopt(CURLOPT_HEADERFUNCTION, curlWriteHeader)
-  if "Accept-Encoding" in request.headers:
-    let s = request.headers["Accept-Encoding"]
-    curl.setopt(CURLOPT_ACCEPT_ENCODING, cstring(s))
-  if request.proxy != nil:
-    let purl = request.proxy.serialize()
-    curl.setopt(CURLOPT_PROXY, purl)
-  case request.httpmethod
-  of HTTP_GET:
-    curl.setopt(CURLOPT_HTTPGET, 1)
-  of HTTP_POST:
-    curl.setopt(CURLOPT_POST, 1)
-    curl.applyPostBody(request, handleData)
-  else: discard #TODO
-  for k, v in request.headers:
-    let header = k & ": " & v
-    handleData.slist = curl_slist_append(handleData.slist, cstring(header))
-  if handleData.slist != nil:
-    curl.setopt(CURLOPT_HTTPHEADER, handleData.slist)
-  let res = curl_multi_add_handle(curlm, curl)
-  if res != CURLM_OK:
-    discard handle.sendResult(int(res))
-    return nil
-  return handleData
diff --git a/src/loader/loader.nim b/src/loader/loader.nim
index 2116a403..acc7817a 100644
--- a/src/loader/loader.nim
+++ b/src/loader/loader.nim
@@ -11,15 +11,15 @@
 #
 # 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 bindings/curl
+import std/nativesockets
+import std/net
+import std/options
+import std/posix
+import std/selectors
+import std/streams
+import std/strutils
+import std/tables
+
 import io/posixstream
 import io/promise
 import io/serialize
@@ -30,9 +30,7 @@ import js/error
 import js/javascript
 import loader/cgi
 import loader/connecterror
-import loader/curlhandle
 import loader/headers
-import loader/http
 import loader/loaderhandle
 import loader/request
 import loader/response
@@ -81,12 +79,11 @@ type
     refcount: int
     ssock: ServerSocket
     alive: bool
-    curlm: CURLM
     config: LoaderConfig
-    extra_fds: seq[curl_waitfd]
-    handleList: seq[CurlHandle]
     handleMap: Table[int, LoaderHandle]
     referrerpolicy: ReferrerPolicy
+    selector: Selector[int]
+    fd: int
 
   LoaderConfig* = object
     defaultheaders*: Headers
@@ -102,12 +99,6 @@ type
 
   FetchPromise* = Promise[JSResult[Response]]
 
-proc addFd(ctx: LoaderContext, fd: int, flags: int) =
-  ctx.extra_fds.add(curl_waitfd(
-    fd: cast[cint](fd),
-    events: cast[cshort](flags)
-  ))
-
 #TODO this may be too low if we want to use urimethodmap for everything
 const MaxRewrites = 4
 
@@ -134,14 +125,18 @@ proc loadResource(ctx: LoaderContext, request: Request, handle: LoaderHandle) =
           inc tries
           redo = true
           continue
-    case request.url.scheme
-    of "http", "https":
-      let handleData = handle.loadHttp(ctx.curlm, request)
-      if handleData != nil:
-        ctx.handleList.add(handleData)
-    of "cgi-bin":
+    if request.url.scheme == "cgi-bin":
       handle.loadCGI(request, ctx.config.cgiDir, prevurl)
-      handle.close()
+      if handle.istream == nil:
+        handle.close()
+      else:
+        let fd = handle.istream.fd
+        ctx.selector.registerHandle(fd, {Read}, 0)
+        let ofl = fcntl(fd, F_GETFL, 0)
+        discard fcntl(fd, F_SETFL, ofl or O_NONBLOCK)
+        # yes, this puts the istream fd in addition to the ostream fd in
+        # handlemap to point to the same ref
+        ctx.handleMap[fd] = handle
     else:
       prevurl = request.url
       case ctx.config.urimethodmap.findAndRewrite(request.url)
@@ -234,39 +229,24 @@ proc acceptConnection(ctx: LoaderContext) =
     # (TODO: this is probably not a very good idea.)
     stream.close()
 
-proc finishCurlTransfer(ctx: LoaderContext, handleData: CurlHandle, res: int) =
-  if res != int(CURLE_OK):
-    discard handleData.handle.sendResult(int(res))
-  if handleData.finish != nil:
-    handleData.finish(handleData)
-  discard curl_multi_remove_handle(ctx.curlm, handleData.curl)
-  handleData.cleanup()
-
 proc exitLoader(ctx: LoaderContext) =
-  for handleData in ctx.handleList:
-    ctx.finishCurlTransfer(handleData, ERROR_LOADER_KILLED)
-  discard curl_multi_cleanup(ctx.curlm)
-  curl_global_cleanup()
   ctx.ssock.close()
   quit(0)
 
 var gctx: LoaderContext
 proc initLoaderContext(fd: cint, config: LoaderConfig): LoaderContext =
-  if curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK:
-    raise newException(Defect, "Failed to initialize libcurl.")
-  let curlm = curl_multi_init()
-  if curlm == nil:
-    raise newException(Defect, "Failed to initialize multi handle.")
   var ctx = LoaderContext(
     alive: true,
-    curlm: curlm,
     config: config,
-    refcount: 1
+    refcount: 1,
+    selector: newSelector[int]()
   )
   gctx = ctx
   #TODO ideally, buffered would be true. Unfortunately this conflicts with
   # sendFileHandle/recvFileHandle.
   ctx.ssock = initServerSocket(buffered = false)
+  ctx.fd = int(ctx.ssock.sock.getFd())
+  ctx.selector.registerHandle(ctx.fd, {Read}, 0)
   # The server has been initialized, so the main process can resume execution.
   var writef: File
   if not open(writef, FileHandle(fd), fmWrite):
@@ -278,7 +258,6 @@ proc initLoaderContext(fd: cint, config: LoaderConfig): LoaderContext =
   onSignal SIGTERM, SIGINT:
     discard sig
     gctx.exitLoader()
-  ctx.addFd(int(ctx.ssock.sock.getFd()), CURL_WAIT_POLLIN)
   for dir in ctx.config.cgiDir.mitems:
     if dir.len > 0 and dir[^1] != '/':
       dir &= '/'
@@ -286,31 +265,40 @@ proc initLoaderContext(fd: cint, config: LoaderConfig): LoaderContext =
 
 proc runFileLoader*(fd: cint, config: LoaderConfig) =
   var ctx = initLoaderContext(fd, config)
+  var buffer {.noInit.}: array[16384, uint8]
   while ctx.alive:
-    var numfds: cint = 0
-    #TODO do not discard
-    discard curl_multi_poll(ctx.curlm, addr ctx.extra_fds[0],
-      cuint(ctx.extra_fds.len), 30_000, addr numfds)
-    discard curl_multi_perform(ctx.curlm, addr numfds)
-    for extra_fd in ctx.extra_fds.mitems:
-      # For now, this is always ssock.sock.getFd().
-      if extra_fd.events == extra_fd.revents:
-        ctx.acceptConnection()
-        extra_fd.revents = 0
-    var msgs_left: cint = 1
-    while msgs_left > 0:
-      let msg = curl_multi_info_read(ctx.curlm, addr msgs_left)
-      if msg == nil:
-        break
-      if msg.msg == CURLMSG_DONE: # the only possible value atm
-        var idx = -1
-        for i in 0 ..< ctx.handleList.len:
-          if ctx.handleList[i].curl == msg.easy_handle:
-            idx = i
-            break
-        assert idx != -1
-        ctx.finishCurlTransfer(ctx.handleList[idx], int(msg.data.result))
-        ctx.handleList.del(idx)
+    let events = ctx.selector.select(-1)
+    var unreg: seq[int]
+    for event in events:
+      if Read in event.events:
+        if event.fd == ctx.fd: # incoming connection
+          ctx.acceptConnection()
+        else:
+          let handle = ctx.handleMap[event.fd]
+          while not handle.istream.atEnd:
+            try:
+              let n = handle.istream.readData(addr buffer[0], buffer.len)
+              if not handle.sendData(addr buffer[0], n):
+                unreg.add(event.fd)
+                break
+            except ErrorAgain, ErrorWouldBlock:
+              break
+      if Error in event.events:
+        assert event.fd != ctx.fd
+        when defined(debug):
+          # sanity check
+          let handle = ctx.handleMap[event.fd]
+          if not handle.istream.atEnd():
+            let n = handle.istream.readData(addr buffer[0], buffer.len)
+            assert n == 0
+            assert handle.istream.atEnd()
+        unreg.add(event.fd)
+    for fd in unreg:
+      ctx.selector.unregister(fd)
+      let handle = ctx.handleMap[fd]
+      ctx.handleMap.del(fd)
+      ctx.handleMap.del(handle.getFd())
+      handle.close()
   ctx.exitLoader()
 
 proc getAttribute(contentType, attrname: string): string =
diff --git a/src/loader/loaderhandle.nim b/src/loader/loaderhandle.nim
index d8d01bb2..93367607 100644
--- a/src/loader/loaderhandle.nim
+++ b/src/loader/loaderhandle.nim
@@ -9,6 +9,8 @@ import loader/headers
 
 type LoaderHandle* = ref object
   ostream: Stream
+  # Stream for taking input
+  istream*: PosixStream
   # 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.
@@ -100,3 +102,5 @@ proc close*(handle: LoaderHandle) =
       discard
     handle.sostream.close()
   handle.ostream.close()
+  if handle.istream != nil:
+    handle.istream.close()
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index cdbfc16b..77404c7c 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -315,7 +315,7 @@ func getClickable(styledNode: StyledNode): Element =
       return Element(styledNode.node)
     styledNode = stylednode.parent
 
-func submitForm(form: HTMLFormElement, submitter: Element): Option[Request]
+proc submitForm(form: HTMLFormElement, submitter: Element): Option[Request]
 
 func canSubmitOnClick(fae: FormAssociatedElement): bool =
   if fae.form == nil:
@@ -330,7 +330,7 @@ func canSubmitOnClick(fae: FormAssociatedElement): bool =
       return true
   return false
 
-func getClickHover(styledNode: StyledNode): string =
+proc getClickHover(styledNode: StyledNode): string =
   let clickable = styledNode.getClickable()
   if clickable != nil:
     case clickable.tagType
@@ -1084,7 +1084,7 @@ proc dispatchEvent(buffer: Buffer, ctype: string, elem: Element): tuple[
       break
   return (called, canceled)
 
-const BufferSize = 4096
+const BufferSize = 16384
 
 proc finishLoad(buffer: Buffer): EmptyPromise =
   if buffer.state != LOADING_PAGE:
@@ -1148,7 +1148,7 @@ proc onload(buffer: Buffer) =
   of LOADING_PAGE:
     discard
   let op = buffer.sstream.getPosition()
-  var s = newSeqUninitialized[uint8](buffer.readbufsize)
+  var s {.noInit.}: array[16384, uint8]
   try:
     buffer.sstream.setPosition(op + buffer.available)
     let n = buffer.istream.readData(addr s[0], buffer.readbufsize)
@@ -1222,7 +1222,7 @@ proc serializePlainTextFormData(kvs: seq[(string, string)]): string =
     result &= "\r\n"
 
 # https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
-func submitForm(form: HTMLFormElement, submitter: Element): Option[Request] =
+proc submitForm(form: HTMLFormElement, submitter: Element): Option[Request] =
   if form.constructingEntryList:
     return
   let entrylist = form.constructEntryList(submitter).get(@[])
@@ -1352,7 +1352,7 @@ type ReadSuccessResult* = object
   open*: Option[Request]
   repaint*: bool
 
-func implicitSubmit(input: HTMLInputElement): Option[Request] =
+proc implicitSubmit(input: HTMLInputElement): Option[Request] =
   let form = input.form
   if form != nil and form.canSubmitImplicitly():
     var defaultButton: Element
diff --git a/src/types/blob.nim b/src/types/blob.nim
index 9ddca2b5..5da7317d 100644
--- a/src/types/blob.nim
+++ b/src/types/blob.nim
@@ -1,5 +1,6 @@
-import options
-import strutils
+import std/options
+import std/os
+import std/strutils
 
 import js/dict
 import js/fromjs
@@ -92,12 +93,14 @@ proc newWebFile(ctx: JSContext, fileBits: seq[string], fileName: string,
 
 #TODO File, Blob constructors
 
-func size*(this: WebFile): uint64 {.jsfget.} =
-  #TODO use stat instead
+proc getSize*(this: Blob): uint64 =
   if this.isfile:
-    return uint64(this.file.getFileSize())
+    return uint64(WebFile(this).path.getFileSize())
   return this.size
 
+proc size*(this: WebFile): uint64 {.jsfget.} =
+  return this.getSize()
+
 func name*(this: WebFile): string {.jsfget.} =
   if this.path.len > 0 and this.path[^1] != '/':
     return this.path.afterLast('/')
diff --git a/src/types/formdata.nim b/src/types/formdata.nim
index 2bc26e22..b1957998 100644
--- a/src/types/formdata.nim
+++ b/src/types/formdata.nim
@@ -1,5 +1,9 @@
+import std/streams
+import std/strutils
+
 import js/javascript
 import types/blob
+import utils/twtstr
 
 type
   FormDataEntry* = object
@@ -13,9 +17,71 @@ type
 
   FormData* = ref object
     entries*: seq[FormDataEntry]
+    boundary*: string
 
 jsDestructor(FormData)
 
 iterator items*(this: FormData): FormDataEntry {.inline.} =
   for entry in this.entries:
     yield entry
+
+proc calcLength*(this: FormData): int =
+  result = 0
+  for entry in this.entries:
+    result += "--\r\n".len + this.boundary.len # always have boundary
+    #TODO maybe make CRLF for name first?
+    result += entry.name.len # always have name
+    # these must be percent-encoded, with 2 char overhead:
+    result += entry.name.count({'\r', '\n', '"'}) * 2
+    if entry.isstr:
+      result += "Content-Disposition: form-data; name=\"\"\r\n".len
+      result += entry.svalue.len
+    else:
+      result += "Content-Disposition: form-data; name=\"\";".len
+      # file name
+      result += " filename=\"\"\r\n".len
+      result += entry.filename.len
+      # dquot must be quoted with 2 char overhead
+      result += entry.filename.count('"') * 2
+      # content type
+      result += "Content-Type: \r\n".len
+      result += entry.value.ctype.len
+      if entry.value.isfile:
+        result += int(WebFile(entry.value).getSize())
+      else:
+        result += int(entry.value.size)
+    result += "\r\n".len # header is always followed by \r\n
+    result += "\r\n".len # value is always followed by \r\n
+
+proc getContentType*(this: FormData): string =
+  return "multipart/form-data; boundary=" & this.boundary
+
+proc writeEntry*(stream: Stream, entry: FormDataEntry, boundary: string) =
+  stream.write("--" & boundary & "\r\n")
+  let name = percentEncode(entry.name, {'"', '\r', '\n'})
+  if entry.isstr:
+    stream.write("Content-Disposition: form-data; name=\"" & name & "\"\r\n")
+    stream.write("\r\n")
+    stream.write(entry.svalue)
+  else:
+    stream.write("Content-Disposition: form-data; name=\"" & name & "\";")
+    let filename = percentEncode(entry.filename, {'"', '\r', '\n'})
+    stream.write(" filename=\"" & filename & "\"\r\n")
+    let blob = entry.value
+    let ctype = if blob.ctype == "":
+      "application/octet-stream"
+    else:
+      blob.ctype
+    stream.write("Content-Type: " & ctype & "\r\n")
+    if blob.isfile:
+      let fs = newFileStream(WebFile(blob).path)
+      if fs != nil:
+        var buf {.noInit.}: array[4096, uint8]
+        while true:
+          let n = fs.readData(addr buf[0], 4096)
+          stream.writeData(addr buf[0], n)
+          if n != 4096: break
+    else:
+      stream.writeData(blob.buffer, int(blob.size))
+    stream.write("\r\n")
+  stream.write("\r\n")
diff --git a/src/xhr/formdata.nim b/src/xhr/formdata.nim
index 84c13402..98c96b54 100644
--- a/src/xhr/formdata.nim
+++ b/src/xhr/formdata.nim
@@ -1,3 +1,6 @@
+import std/base64
+import std/streams
+
 import html/dom
 import html/enums
 import js/domexception
@@ -12,12 +15,20 @@ import chame/tags
 proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil,
     encoding: string = ""): Option[seq[FormDataEntry]]
 
+
+proc generateBoundary(): string =
+  let urandom = newFileStream("/dev/urandom")
+  let s = urandom.readStr(32)
+  urandom.close()
+  # 32 * 4 / 3 (padded) = 44 + prefix string is 22 bytes = 66 bytes
+  return "----WebKitFormBoundary" & base64.encode(s)
+
 proc newFormData0*(): FormData =
-  return FormData()
+  return FormData(boundary: generateBoundary())
 
 proc newFormData*(form: HTMLFormElement = nil,
     submitter: HTMLElement = nil): DOMResult[FormData] {.jsctor.} =
-  let this = FormData()
+  let this = newFormData0()
   if form != nil:
     if submitter != nil:
       if not submitter.isSubmitButton():