about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/html/dom.nim57
-rw-r--r--src/html/enums.nim12
-rw-r--r--src/html/formdata.nim22
-rw-r--r--src/io/bufreader.nim74
-rw-r--r--src/io/bufstream.nim4
-rw-r--r--src/io/bufwriter.nim83
-rw-r--r--src/loader/cgi.nim1
-rw-r--r--src/local/client.nim11
-rw-r--r--src/local/container.nim40
-rw-r--r--src/local/pager.nim18
-rw-r--r--src/server/buffer.nim68
-rw-r--r--src/server/forkserver.nim11
-rw-r--r--src/types/blob.nim44
-rw-r--r--src/types/formdata.nim32
14 files changed, 244 insertions, 233 deletions
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 5daba32e..3e93de79 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -51,7 +51,9 @@ import chame/tags
 
 type
   FormMethod* = enum
-    fmGet, fmPost, fmDialog
+    fmGet = "get"
+    fmPost = "post"
+    fmDialog = "dialog"
 
   FormEncodingType* = enum
     fetUrlencoded = "application/x-www-form-urlencoded",
@@ -245,7 +247,7 @@ type
     checked* {.jsget.}: bool
     xcoord*: int
     ycoord*: int
-    file*: Option[URL]
+    file*: WebFile
 
   HTMLAnchorElement* = ref object of HTMLElement
     relList {.jsget.}: DOMTokenList
@@ -281,7 +283,6 @@ type
     fetchStarted: bool
 
   HTMLFormElement* = ref object of HTMLElement
-    enctype*: string
     constructingEntryList*: bool
     controls*: seq[FormAssociatedElement]
     relList {.jsget.}: DOMTokenList
@@ -2231,19 +2232,18 @@ func inputString*(input: HTMLInputElement): string =
   of itPassword:
     '*'.repeat(input.value.len).padToWidth(int(input.attrulgz(satSize).get(20)))
   of itReset:
-    if input.value != "": input.value
-    else: "RESET"
+    if input.value != "":
+      input.value
+    else:
+      "RESET"
   of itSubmit, itButton:
     if input.value != "":
       input.value
     else:
       "SUBMIT"
   of itFile:
-    if input.file.isNone:
-      "".padToWidth(int(input.attrulgz(satSize).get(20)))
-    else:
-      input.file.get.path.serialize_unicode()
-        .padToWidth(int(input.attrulgz(satSize).get(20)))
+    let s = if input.file != nil: input.file.name else: ""
+    s.padToWidth(int(input.attrulgz(satSize).get(20)))
   else: input.value
 
 func textAreaString*(textarea: HTMLTextAreaElement): string =
@@ -2281,30 +2281,25 @@ func action*(element: Element): string =
   return ""
 
 func enctype*(element: Element): FormEncodingType =
+  if element of HTMLFormElement:
+    # Note: see below, this is not in the standard.
+    if element.attrb(satEnctype):
+      let s = element.attr(satEnctype)
+      return parseEnumNoCase[FormEncodingType](s).get(fetUrlencoded)
   if element.isSubmitButton():
     if element.attrb(satFormenctype):
-      return case element.attr(satFormenctype).toLowerAscii()
-      of "application/x-www-form-urlencoded": fetUrlencoded
-      of "multipart/form-data": fetMultipart
-      of "text/plain": fetTextPlain
-      else: fetUrlencoded
-  if element of HTMLInputElement:
-    let element = HTMLInputElement(element)
-    if element.form != nil:
-      if element.form.attrb(satEnctype):
-        return case element.attr(satEnctype).toLowerAscii()
-        of "application/x-www-form-urlencoded": fetUrlencoded
-        of "multipart/form-data": fetMultipart
-        of "text/plain": fetTextPlain
-        else: fetUrlencoded
+      let s = element.attr(satFormenctype)
+      return parseEnumNoCase[FormEncodingType](s).get(fetUrlencoded)
+  if element of FormAssociatedElement:
+    let element = FormAssociatedElement(element)
+    if (let form = element.form; form != nil):
+      if form.attrb(satEnctype):
+        let s = form.attr(satEnctype)
+        return parseEnumNoCase[FormEncodingType](s).get(fetUrlencoded)
   return fetUrlencoded
 
 func parseFormMethod(s: string): FormMethod =
-  return case s.toLowerAscii()
-  of "get": fmGet
-  of "post": fmPost
-  of "dialog": fmDialog
-  else: fmGet
+  return parseEnumNoCase[FormMethod](s).get(fmGet)
 
 func formmethod*(element: Element): FormMethod =
   if element of HTMLFormElement:
@@ -2951,7 +2946,7 @@ proc reflectAttrs(element: Element; name: CAtom; value: string) =
     input.reflect_str satValue, value
     input.reflect_bool satChecked, checked
     if name == satType:
-      input.inputType = inputType(value)
+      input.inputType = parseEnumNoCase[InputType](value).get(itText)
   of TAG_OPTION:
     let option = HTMLOptionElement(element)
     option.reflect_bool satSelected, selected
@@ -3223,7 +3218,7 @@ proc resetElement*(element: Element) =
     of itCheckbox, itRadio:
       input.checked = input.attrb(satChecked)
     of itFile:
-      input.file = none(URL)
+      input.file = nil
     else:
       input.value = input.attr(satValue)
     input.invalid = true
diff --git a/src/html/enums.nim b/src/html/enums.nim
index 925e3b68..119a9a9b 100644
--- a/src/html/enums.nim
+++ b/src/html/enums.nim
@@ -1,6 +1,3 @@
-import std/strutils
-import std/tables
-
 import chame/tags
 
 type
@@ -71,15 +68,6 @@ const ResettableElements* = {
   TAG_INPUT, TAG_OUTPUT, TAG_SELECT, TAG_TEXTAREA
 }
 
-func getInputTypeMap(): Table[string, InputType] =
-  for i in InputType:
-    result[$InputType(i)] = InputType(i)
-
-const inputTypeMap = getInputTypeMap()
-
-func inputType*(s: string): InputType =
-  return inputTypeMap.getOrDefault(s.toLowerAscii())
-
 const AutoDirInput* = {
   itHidden, itText, itSearch, itTel, itURL, itEmail, itPassword, itSubmit,
   itReset, itButton
diff --git a/src/html/formdata.nim b/src/html/formdata.nim
index 335e36b6..7f099acd 100644
--- a/src/html/formdata.nim
+++ b/src/html/formdata.nim
@@ -1,8 +1,8 @@
-import std/streams
-
 import html/catom
 import html/dom
 import html/enums
+import io/dynstream
+import io/posixstream
 import js/base64
 import js/domexception
 import js/javascript
@@ -16,11 +16,12 @@ import chame/tags
 proc constructEntryList*(form: HTMLFormElement; submitter: Element = nil;
     encoding = "UTF-8"): seq[FormDataEntry]
 
+var urandom* {.global.}: PosixStream
+
 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
+  var s: array[33, uint8]
+  urandom.recvDataLoop(s)
+  # 33 * 4 / 3 = 44 + prefix string is 22 bytes = 66 bytes
   return "----WebKitFormBoundary" & btoa(s)
 
 proc newFormData0*(): FormData =
@@ -147,8 +148,13 @@ proc constructEntryList*(form: HTMLFormElement; submitter: Element = nil;
           "on"
         entrylist.add((name, value))
       of itFile:
-        #TODO file
-        discard
+        if field.file != nil:
+          entrylist.add(FormDataEntry(
+            name: name,
+            filename: field.file.name,
+            isstr: false,
+            value: field.file
+          ))
       of itHidden:
         if name.equalsIgnoreCase("_charset_"):
           entrylist.add((name, encoding))
diff --git a/src/io/bufreader.nim b/src/io/bufreader.nim
index f25309e2..90940424 100644
--- a/src/io/bufreader.nim
+++ b/src/io/bufreader.nim
@@ -1,10 +1,9 @@
-# Write data to streams.
-
 import std/options
 import std/sets
 import std/tables
 
 import io/dynstream
+import io/socketstream
 import types/blob
 import types/color
 import types/formdata
@@ -14,29 +13,7 @@ import types/url
 type BufferedReader* = object
   buffer: seq[uint8]
   bufIdx: int
-
-proc initReader*(stream: DynStream; len: int): BufferedReader =
-  assert len != 0
-  var reader = BufferedReader(
-    buffer: newSeqUninitialized[uint8](len),
-    bufIdx: 0
-  )
-  var n = 0
-  while true:
-    n += stream.recvData(addr reader.buffer[n], len - n)
-    if n == len:
-      break
-  return reader
-
-proc initPacketReader*(stream: DynStream): BufferedReader =
-  var len: int
-  stream.recvDataLoop(addr len, sizeof(len))
-  return stream.initReader(len)
-
-template withPacketReader*(stream: DynStream; r, body: untyped) =
-  block:
-    var r = stream.initPacketReader()
-    body
+  recvAux: seq[FileHandle] #TODO assert on unused ones
 
 proc sread*(reader: var BufferedReader; n: var SomeNumber)
 proc sread*[T](reader: var BufferedReader; s: var set[T])
@@ -56,6 +33,27 @@ proc sread*[T](reader: var BufferedReader; o: var Option[T])
 proc sread*[T, E](reader: var BufferedReader; o: var Result[T, E])
 proc sread*(reader: var BufferedReader; c: var ARGBColor) {.inline.}
 
+proc initReader*(stream: DynStream; len, auxLen: int): BufferedReader =
+  assert len != 0
+  var reader = BufferedReader(
+    buffer: newSeqUninitialized[uint8](len),
+    bufIdx: 0
+  )
+  stream.recvDataLoop(reader.buffer)
+  for i in 0 ..< auxLen:
+    reader.recvAux.add(SocketStream(stream).recvFileHandle())
+  return reader
+
+proc initPacketReader*(stream: DynStream): BufferedReader =
+  var len: array[2, int]
+  stream.recvDataLoop(addr len[0], sizeof(len))
+  return stream.initReader(len[0], len[1])
+
+template withPacketReader*(stream: DynStream; r, body: untyped) =
+  block:
+    var r = stream.initPacketReader()
+    body
+
 proc readData(reader: var BufferedReader; buffer: pointer; len: int) =
   assert reader.bufIdx + len <= reader.buffer.len
   copyMem(buffer, addr reader.buffer[reader.bufIdx], len)
@@ -156,22 +154,22 @@ proc sread*(reader: var BufferedReader; part: var FormDataEntry) =
     reader.sread(part.value)
 
 proc sread*(reader: var BufferedReader; blob: var Blob) =
-  var isfile: bool
-  reader.sread(isfile)
-  if isfile:
-    var file = new WebFile
-    file.isfile = true
-    reader.sread(file.path)
-    blob = file
-  else:
-    blob = Blob()
-    reader.sread(blob.ctype)
-    reader.sread(blob.size)
+  var isWebFile: bool
+  reader.sread(isWebFile)
+  blob = if isWebFile: WebFile() else: Blob()
+  if isWebFile:
+    reader.sread(WebFile(blob).name)
+  var hasFd: bool
+  reader.sread(hasFd)
+  if hasFd:
+    blob.fd = some(reader.recvAux.pop())
+  reader.sread(blob.ctype)
+  reader.sread(blob.size)
+  if blob.size > 0:
     let buffer = alloc(blob.size)
+    reader.readData(blob.buffer, int(blob.size))
     blob.buffer = buffer
     blob.deallocFun = proc() = dealloc(buffer)
-    if blob.size > 0:
-      reader.readData(blob.buffer, int(blob.size))
 
 proc sread*[T](reader: var BufferedReader; o: var Option[T]) =
   var x: bool
diff --git a/src/io/bufstream.nim b/src/io/bufstream.nim
index 62a0df3e..3dba86f1 100644
--- a/src/io/bufstream.nim
+++ b/src/io/bufstream.nim
@@ -45,5 +45,9 @@ proc flushWrite*(s: BufStream): bool =
   s.writeBuffer = s.writeBuffer.substr(n)
   return false
 
+proc reallyFlush*(s: BufStream) =
+  if s.writeBuffer.len > 0:
+    s.source.sendDataLoop(s.writeBuffer)
+
 proc newBufStream*(ps: PosixStream; registerFun: proc(fd: int)): BufStream =
   return BufStream(source: ps, blocking: ps.blocking, registerFun: registerFun)
diff --git a/src/io/bufwriter.nim b/src/io/bufwriter.nim
index 20ef1f4c..fd3c12a8 100644
--- a/src/io/bufwriter.nim
+++ b/src/io/bufwriter.nim
@@ -6,6 +6,7 @@ import std/sets
 import std/tables
 
 import io/dynstream
+import io/socketstream
 import types/blob
 import types/color
 import types/formdata
@@ -17,7 +18,7 @@ type BufferedWriter* = object
   buffer: ptr UncheckedArray[uint8]
   bufSize: int
   bufLen: int
-  writeLen: bool
+  sendAux: seq[FileHandle]
 
 {.warning[Deprecated]: off.}:
   proc `=destroy`(writer: var BufferedWriter) =
@@ -25,26 +26,42 @@ type BufferedWriter* = object
       dealloc(writer.buffer)
       writer.buffer = nil
 
-proc initWriter*(stream: DynStream; sizeInit = 64; writeLen = false):
+proc swrite*(writer: var BufferedWriter; n: SomeNumber)
+proc swrite*[T](writer: var BufferedWriter; s: set[T])
+proc swrite*[T: enum](writer: var BufferedWriter; x: T)
+proc swrite*(writer: var BufferedWriter; s: string)
+proc swrite*(writer: var BufferedWriter; b: bool)
+proc swrite*(writer: var BufferedWriter; url: URL)
+proc swrite*(writer: var BufferedWriter; tup: tuple)
+proc swrite*[I, T](writer: var BufferedWriter; a: array[I, T])
+proc swrite*(writer: var BufferedWriter; s: seq)
+proc swrite*[U, V](writer: var BufferedWriter; t: Table[U, V])
+proc swrite*(writer: var BufferedWriter; obj: object)
+proc swrite*(writer: var BufferedWriter; obj: ref object)
+proc swrite*(writer: var BufferedWriter; part: FormDataEntry)
+proc swrite*(writer: var BufferedWriter; blob: Blob)
+proc swrite*[T](writer: var BufferedWriter; o: Option[T])
+proc swrite*[T, E](writer: var BufferedWriter; o: Result[T, E])
+proc swrite*(writer: var BufferedWriter; c: ARGBColor) {.inline.}
+
+const InitLen = sizeof(int) * 2
+const SizeInit = max(64, InitLen)
+proc initWriter*(stream: DynStream):
     BufferedWriter =
-  var w = BufferedWriter(
+  return BufferedWriter(
     stream: stream,
-    buffer: cast[ptr UncheckedArray[uint8]](alloc(sizeInit)),
-    bufSize: sizeInit,
-    bufLen: 0,
-    writeLen: writeLen
+    buffer: cast[ptr UncheckedArray[uint8]](alloc(SizeInit)),
+    bufSize: SizeInit,
+    bufLen: InitLen
   )
-  if writeLen: # add space for `len'
-    w.bufLen += sizeof(w.bufLen)
-    assert w.bufLen < sizeInit
-  return w
 
 proc flush*(writer: var BufferedWriter) =
-  if writer.writeLen:
-    # subtract the length field's size
-    var realLen = writer.bufLen - sizeof(writer.bufLen)
-    copyMem(writer.buffer, addr realLen, sizeof(writer.bufLen))
+  # subtract the length field's size
+  let len = [writer.bufLen - InitLen, writer.sendAux.len]
+  copyMem(writer.buffer, unsafeAddr len[0], sizeof(len))
   writer.stream.sendDataLoop(writer.buffer, writer.bufLen)
+  for i in countdown(writer.sendAux.high, 0):
+    SocketStream(writer.stream).sendFileHandle(writer.sendAux[i])
   writer.bufLen = 0
   writer.stream.sflush()
 
@@ -53,32 +70,15 @@ proc deinit*(writer: var BufferedWriter) =
   writer.buffer = nil
   writer.bufSize = 0
   writer.bufLen = 0
+  writer.sendAux.setLen(0)
 
 template withPacketWriter*(stream: DynStream; w, body: untyped) =
   block:
-    var w = stream.initWriter(writeLen = true)
+    var w = stream.initWriter()
     body
     w.flush()
     w.deinit()
 
-proc swrite*(writer: var BufferedWriter; n: SomeNumber)
-proc swrite*[T](writer: var BufferedWriter; s: set[T])
-proc swrite*[T: enum](writer: var BufferedWriter; x: T)
-proc swrite*(writer: var BufferedWriter; s: string)
-proc swrite*(writer: var BufferedWriter; b: bool)
-proc swrite*(writer: var BufferedWriter; url: URL)
-proc swrite*(writer: var BufferedWriter; tup: tuple)
-proc swrite*[I, T](writer: var BufferedWriter; a: array[I, T])
-proc swrite*(writer: var BufferedWriter; s: seq)
-proc swrite*[U, V](writer: var BufferedWriter; t: Table[U, V])
-proc swrite*(writer: var BufferedWriter; obj: object)
-proc swrite*(writer: var BufferedWriter; obj: ref object)
-proc swrite*(writer: var BufferedWriter; part: FormDataEntry)
-proc swrite*(writer: var BufferedWriter; blob: Blob)
-proc swrite*[T](writer: var BufferedWriter; o: Option[T])
-proc swrite*[T, E](writer: var BufferedWriter; o: Result[T, E])
-proc swrite*(writer: var BufferedWriter; c: ARGBColor) {.inline.}
-
 proc writeData(writer: var BufferedWriter; buffer: pointer; len: int) =
   let targetLen = writer.bufLen + len
   let missing = targetLen - writer.bufSize
@@ -161,12 +161,15 @@ proc swrite*(writer: var BufferedWriter; part: FormDataEntry) =
 
 #TODO clean up this mess
 proc swrite*(writer: var BufferedWriter; blob: Blob) =
-  writer.swrite(blob.isfile)
-  if blob.isfile:
-    writer.swrite(WebFile(blob).path)
-  else:
-    writer.swrite(blob.ctype)
-    writer.swrite(blob.size)
+  if blob.fd.isSome:
+    writer.sendAux.add(blob.fd.get)
+  writer.swrite(blob of WebFile)
+  if blob of WebFile:
+    writer.swrite(WebFile(blob).name)
+  writer.swrite(blob.fd.isSome)
+  writer.swrite(blob.ctype)
+  writer.swrite(blob.size)
+  if blob.size > 0:
     writer.writeData(blob.buffer, int(blob.size))
 
 proc swrite*[T](writer: var BufferedWriter; o: Option[T]) =
diff --git a/src/loader/cgi.nim b/src/loader/cgi.nim
index 2c395e95..347855ac 100644
--- a/src/loader/cgi.nim
+++ b/src/loader/cgi.nim
@@ -229,6 +229,7 @@ proc loadCGI*(handle: LoaderHandle; request: Request; cgiDir: seq[string];
         let multipart = request.multipart.get
         for entry in multipart.entries:
           ps.writeEntry(entry, multipart.boundary)
+        ps.writeEnd(multipart.boundary)
       ps.sclose()
     handle.parser = HeaderParser(headers: newHeaders())
     handle.istream = newPosixStream(pipefd[0])
diff --git a/src/local/client.nim b/src/local/client.nim
index af5e89a0..dc79438e 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -658,11 +658,12 @@ proc clientLoadJSModule(ctx: JSContext; module_name: cstringConst;
     JS_ThrowTypeError(ctx, "Failed to open file %s", module_name)
     return nil
 
-proc readBlob(client: Client; path: string): Option[WebFile] {.jsfunc.} =
-  try:
-    return some(newWebFile(path))
-  except IOError:
-    discard
+proc readBlob(client: Client; path: string): WebFile {.jsfunc.} =
+  let ps = newPosixStream(path, O_RDONLY, 0)
+  if ps == nil:
+    return nil
+  let name = path.afterLast('/')
+  return newWebFile(name, ps.fd)
 
 #TODO this is dumb
 proc readFile(client: Client; path: string): string {.jsfunc.} =
diff --git a/src/local/container.nim b/src/local/container.nim
index 502b1de1..5b593f2a 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -7,6 +7,7 @@ import std/unicode
 
 import config/config
 import config/mimetypes
+import io/bufstream
 import io/dynstream
 import io/promise
 import io/serversocket
@@ -46,8 +47,8 @@ type
     setxsave: bool
 
   ContainerEventType* = enum
-    cetAnchor, cetNoAnchor, cetUpdate, cetReadLine, cetReadArea, cetOpen,
-    cetSetLoadInfo, cetStatus, cetAlert, cetLoaded, cetTitle, cetCancel
+    cetAnchor, cetNoAnchor, cetUpdate, cetReadLine, cetReadArea, cetReadFile,
+    cetOpen, cetSetLoadInfo, cetStatus, cetAlert, cetLoaded, cetTitle, cetCancel
 
   ContainerEvent* = object
     case t*: ContainerEventType
@@ -1483,8 +1484,12 @@ proc readCanceled*(container: Container) =
     if repaint:
       container.needslines = true)
 
-proc readSuccess*(container: Container; s: string) =
-  container.iface.readSuccess(s).then(proc(res: ReadSuccessResult) =
+proc readSuccess*(container: Container; s: string; fd = -1) =
+  let p = container.iface.readSuccess(s, fd != -1)
+  if fd != -1:
+    container.iface.stream.reallyFlush()
+    SocketStream(container.iface.stream.source).sendFileHandle(FileHandle(fd))
+  p.then(proc(res: ReadSuccessResult) =
     if res.repaint:
       container.needslines = true
     if res.open.isSome:
@@ -1521,19 +1526,21 @@ proc onclick(container: Container; res: ClickResult; save: bool) =
     container.displaySelect(res.select.get)
   if res.readline.isSome:
     let rl = res.readline.get
-    let event = if rl.area:
-      ContainerEvent(
-        t: cetReadArea,
-        tvalue: rl.value
-      )
-    else:
-      ContainerEvent(
+    case rl.t
+    of rltText:
+      container.triggerEvent(ContainerEvent(
         t: cetReadLine,
         prompt: rl.prompt,
         value: rl.value,
         password: rl.hide
-      )
-    container.triggerEvent(event)
+      ))
+    of rltArea:
+      container.triggerEvent(ContainerEvent(
+        t: cetReadArea,
+        tvalue: rl.value
+      ))
+    of rltFile:
+      container.triggerEvent(ContainerEvent(t: cetReadFile))
 
 proc click*(container: Container) {.jsfunc.} =
   if container.select.open:
@@ -1601,10 +1608,9 @@ func hoverImage(container: Container): string {.jsfget.} =
   return container.hoverText[htImage]
 
 proc handleCommand(container: Container) =
-  var packetid, len: int
-  container.iface.stream.recvDataLoop(addr len, sizeof(len))
-  container.iface.stream.recvDataLoop(addr packetid, sizeof(packetid))
-  container.iface.resolve(packetid, len - sizeof(packetid))
+  var packet: array[3, int] # 0 len, 1 auxLen, 2 packetid
+  container.iface.stream.recvDataLoop(addr packet[0], sizeof(packet))
+  container.iface.resolve(packet[2], packet[0] - sizeof(packet[2]), packet[1])
 
 proc startLoad(container: Container) =
   container.iface.load().then(proc(res: int) =
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 93bf562c..c1bd18c0 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -65,6 +65,7 @@ type
     lmISearchB = "?"
     lmGotoLine = "Goto line: "
     lmDownload = "(Download)Save file to: "
+    lmBufferFile = "(Upload)Filename: "
 
   # fdin is the original fd; fdout may be the same, or different if mailcap
   # is used.
@@ -1173,6 +1174,19 @@ proc updateReadLine*(pager: Pager) =
         if pager.commandMode:
           pager.command()
       of lmBuffer: pager.container.readSuccess(lineedit.news)
+      of lmBufferFile:
+        let ps = newPosixStream(lineedit.news, O_RDONLY, 0)
+        if ps == nil:
+          pager.alert("File not found")
+          pager.container.readCanceled()
+        else:
+          var stats: Stat
+          if fstat(ps.fd, stats) < 0 or S_ISDIR(stats.st_mode):
+            pager.alert("Not a file: " & lineedit.news)
+          else:
+            let name = lineedit.news.afterLast('/')
+            pager.container.readSuccess(name, ps.fd)
+          ps.sclose()
       of lmSearchF, lmSearchB:
         if lineedit.news != "":
           let regex = pager.compileSearchRegex(lineedit.news)
@@ -1655,7 +1669,6 @@ proc connected(pager: Pager; container: Container; response: Response) =
     container.process = pager.forkserver.forkBuffer(
       container.config,
       container.url,
-      container.request,
       attrs,
       mailcapRes.ishtml,
       container.charsetStack
@@ -1768,6 +1781,9 @@ proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent):
       else:
         pager.container.readCanceled()
       pager.redraw = true
+  of cetReadFile:
+    if container == pager.container:
+      pager.setLineEdit(lmBufferFile, "")
   of cetOpen:
     let url = event.request.url
     if not event.save and (pager.container != container or
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index b9ef513f..4e4d2c0e 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -45,6 +45,7 @@ import js/tojs
 import layout/renderdocument
 import loader/headers
 import loader/loader
+import types/blob
 import types/cell
 import types/color
 import types/cookie
@@ -94,7 +95,6 @@ type
     firstBufferRead: bool
     lines: FlexibleGrid
     images: seq[PosBitmap]
-    request: Request # source request
     attrs: WindowAttributes
     window: Window
     document: Document
@@ -130,6 +130,7 @@ type
   InterfaceOpaque = ref object
     stream: SocketStream
     len: int
+    auxLen: int
 
   BufferInterface* = ref object
     map: PromiseMap
@@ -149,7 +150,7 @@ type
 proc getFromOpaque[T](opaque: pointer; res: var T) =
   let opaque = cast[InterfaceOpaque](opaque)
   if opaque.len != 0:
-    var r = opaque.stream.initReader(opaque.len)
+    var r = opaque.stream.initReader(opaque.len, opaque.auxLen)
     r.sread(res)
     opaque.len = 0
 
@@ -178,8 +179,9 @@ proc cloneInterface*(stream: SocketStream; registerFun: proc(fd: int)):
   r.sread(pid)
   return iface
 
-proc resolve*(iface: BufferInterface; packetid, len: int) =
+proc resolve*(iface: BufferInterface; packetid, len, auxLen: int) =
   iface.opaque.len = len
+  iface.opaque.auxLen = auxLen
   iface.map.resolve(packetid)
   # Protection against accidentally not exhausting data available to read,
   # by setting opaque len to 0 in getFromOpaque.
@@ -226,7 +228,7 @@ proc buildInterfaceProc(fun: NimNode; funid: string):
       let id2 = newIdentDefs(ident(param[i].strVal), param[^2])
       params2.add(id2)
   body.add(quote do:
-    var writer {.inject.} = `thisval`.stream.initWriter(writeLen = true)
+    var writer {.inject.} = `thisval`.stream.initWriter()
     writer.swrite(BufferCommand.`nup`)
     writer.swrite(`thisval`.packetid)
   )
@@ -1435,21 +1437,22 @@ proc implicitSubmit(input: HTMLInputElement): Option[Request] =
     else:
       return submitForm(form, form)
 
-proc readSuccess*(buffer: Buffer; s: string): ReadSuccessResult {.proxy.} =
+proc readSuccess*(buffer: Buffer; s: string; hasFd: bool): ReadSuccessResult
+    {.proxy.} =
+  var fd: FileHandle = -1
+  if hasFd:
+    fd = buffer.pstream.recvFileHandle()
   if buffer.document.focus != nil:
     case buffer.document.focus.tagType
     of TAG_INPUT:
       let input = HTMLInputElement(buffer.document.focus)
       case input.inputType
       of itFile:
-        let cdir = parseURL("file://" & getCurrentDir() & DirSep)
-        let path = parseURL(s, cdir)
-        if path.isSome:
-          input.file = path
-          input.invalid = true
-          buffer.do_reshape()
-          result.repaint = true
-          result.open = implicitSubmit(input)
+        input.file = newWebFile(s, fd)
+        input.invalid = true
+        buffer.do_reshape()
+        result.repaint = true
+        result.open = implicitSubmit(input)
       else:
         input.value = s
         input.invalid = true
@@ -1467,11 +1470,15 @@ proc readSuccess*(buffer: Buffer; s: string): ReadSuccessResult {.proxy.} =
     if not result.repaint:
       result.repaint = r
 
-type ReadLineResult* = object
-  prompt*: string
-  value*: string
-  hide*: bool
-  area*: bool
+type
+  ReadLineType* = enum
+    rltText, rltArea, rltFile
+
+  ReadLineResult* = object
+    t*: ReadLineType
+    prompt*: string
+    value*: string
+    hide*: bool
 
 type
   SelectResult* = object
@@ -1580,8 +1587,8 @@ proc click(buffer: Buffer; button: HTMLButtonElement): ClickResult =
 proc click(buffer: Buffer; textarea: HTMLTextAreaElement): ClickResult =
   let repaint = buffer.setFocus(textarea)
   let readline = ReadLineResult(
-    value: textarea.value,
-    area: true,
+    t: rltArea,
+    value: textarea.value
   )
   return ClickResult(
     readline: some(readline),
@@ -1596,7 +1603,7 @@ const InputTypePrompt = [
   itDate: "Date",
   itDatetimeLocal: "Local date/time",
   itEmail: "E-Mail",
-  itFile: "Filename",
+  itFile: "",
   itHidden: "",
   itImage: "Image",
   itMonth: "Month",
@@ -1617,16 +1624,10 @@ proc click(buffer: Buffer; input: HTMLInputElement): ClickResult =
   let repaint = buffer.restoreFocus()
   case input.inputType
   of itFile:
-    var path = if input.file.isSome:
-      input.file.get.path.serialize_unicode()
-    else:
-      ""
+    #TODO we should somehow extract the path name from the current file
     return ClickResult(
       repaint: buffer.setFocus(input) or repaint,
-      readline: some(ReadLineResult(
-        prompt: InputTypePrompt[itFile] & ": ",
-        value: path
-      ))
+      readline: some(ReadLineResult(t: rltFile))
     )
   of itCheckbox:
     input.checked = not input.checked
@@ -1952,16 +1953,16 @@ proc runBuffer(buffer: Buffer) =
 
 proc cleanup(buffer: Buffer) =
   buffer.pstream.sclose()
+  urandom.sclose()
   # no unlink access on Linux
   when defined(linux):
     buffer.ssock.close(unlink = false)
   else:
     buffer.ssock.close()
 
-proc launchBuffer*(config: BufferConfig; url: URL; request: Request;
-    attrs: WindowAttributes; ishtml: bool; charsetStack: seq[Charset];
-    loader: FileLoader; ssock: ServerSocket; pstream: SocketStream;
-    selector: Selector[int]) =
+proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes;
+    ishtml: bool; charsetStack: seq[Charset]; loader: FileLoader;
+    ssock: ServerSocket; pstream: SocketStream; selector: Selector[int]) =
   let emptySel = Selector[int]()
   emptySel[] = selector[]
   let buffer = Buffer(
@@ -1972,7 +1973,6 @@ proc launchBuffer*(config: BufferConfig; url: URL; request: Request;
     loader: loader,
     needsBOMSniff: config.charsetOverride == CHARSET_UNKNOWN,
     pstream: pstream,
-    request: request,
     rfd: pstream.fd,
     selector: selector,
     ssock: ssock,
diff --git a/src/server/forkserver.nim b/src/server/forkserver.nim
index 7502a481..23204629 100644
--- a/src/server/forkserver.nim
+++ b/src/server/forkserver.nim
@@ -5,6 +5,7 @@ import std/selectors
 import std/tables
 
 import config/config
+import html/formdata
 import io/bufreader
 import io/bufwriter
 import io/dynstream
@@ -60,13 +61,12 @@ proc removeChild*(forkserver: ForkServer; pid: int) =
     w.swrite(pid)
 
 proc forkBuffer*(forkserver: ForkServer; config: BufferConfig; url: URL;
-    request: Request; attrs: WindowAttributes; ishtml: bool;
-    charsetStack: seq[Charset]): int =
+    attrs: WindowAttributes; ishtml: bool; charsetStack: seq[Charset]):
+    int =
   forkserver.ostream.withPacketWriter w:
     w.swrite(fcForkBuffer)
     w.swrite(config)
     w.swrite(url)
-    w.swrite(request)
     w.swrite(attrs)
     w.swrite(ishtml)
     w.swrite(charsetStack)
@@ -121,13 +121,11 @@ proc forkLoader(ctx: var ForkServerContext; config: LoaderConfig): int =
 proc forkBuffer(ctx: var ForkServerContext; r: var BufferedReader): int =
   var config: BufferConfig
   var url: URL
-  var request: Request
   var attrs: WindowAttributes
   var ishtml: bool
   var charsetStack: seq[Charset]
   r.sread(config)
   r.sread(url)
-  r.sread(request)
   r.sread(attrs)
   r.sread(ishtml)
   r.sread(charsetStack)
@@ -161,6 +159,7 @@ proc forkBuffer(ctx: var ForkServerContext; r: var BufferedReader): int =
     let ps = newPosixStream(pipefd[1])
     ps.write(char(0))
     ps.sclose()
+    urandom = newPosixStream("/dev/urandom", O_RDONLY, 0)
     let pstream = ssock.acceptSocketStream()
     gssock = ssock
     gpstream = pstream
@@ -180,7 +179,7 @@ proc forkBuffer(ctx: var ForkServerContext; r: var BufferedReader): int =
       sockDirFd: sockDirFd
     )
     try:
-      launchBuffer(config, url, request, attrs, ishtml, charsetStack, loader,
+      launchBuffer(config, url, attrs, ishtml, charsetStack, loader,
         ssock, pstream, selector)
     except CatchableError:
       let e = getCurrentException()
diff --git a/src/types/blob.nim b/src/types/blob.nim
index ebf5aedf..dd812c02 100644
--- a/src/types/blob.nim
+++ b/src/types/blob.nim
@@ -1,27 +1,25 @@
 import std/options
-import std/os
+import std/posix
 import std/strutils
 
 import js/fromjs
 import js/javascript
 import js/jstypes
 import utils/mimeguess
-import utils/twtstr
 
 type
   DeallocFun = proc() {.closure, raises: [].}
 
   Blob* = ref object of RootObj
-    isfile*: bool
     size* {.jsget.}: uint64
     ctype* {.jsget: "type".}: string
     buffer*: pointer
     deallocFun*: DeallocFun
+    fd*: Option[FileHandle]
 
   WebFile* = ref object of Blob
     webkitRelativePath {.jsget.}: string
-    path*: string
-    file: File #TODO maybe use fd?
+    name* {.jsget.}: string
 
 jsDestructor(Blob)
 jsDestructor(WebFile)
@@ -36,25 +34,20 @@ proc newBlob*(buffer: pointer; size: int; ctype: string;
   )
 
 proc finalize(blob: Blob) {.jsfin.} =
+  if blob.fd.isSome:
+    discard close(blob.fd.get)
   if blob.deallocFun != nil and blob.buffer != nil:
     blob.deallocFun()
     blob.buffer = nil
 
 proc finalize(file: WebFile) {.jsfin.} =
-  if file.deallocFun != nil and file.buffer != nil:
-    file.deallocFun()
-    file.buffer = nil
-
-proc newWebFile*(path: string; webkitRelativePath = ""): WebFile =
-  var file: File
-  if not open(file, path, fmRead):
-    raise newException(IOError, "Failed to open file")
+  Blob(file).finalize()
+
+proc newWebFile*(name: string; fd: FileHandle): WebFile =
   return WebFile(
-    isfile: true,
-    path: path,
-    file: file,
-    ctype: DefaultGuess.guessContentType(path),
-    webkitRelativePath: webkitRelativePath
+    name: name,
+    fd: some(fd),
+    ctype: DefaultGuess.guessContentType(name)
   )
 
 type
@@ -68,8 +61,7 @@ type
 proc newWebFile(ctx: JSContext; fileBits: seq[string]; fileName: string;
     options = FilePropertyBag()): WebFile {.jsctor.} =
   let file = WebFile(
-    isfile: false,
-    path: fileName,
+    name: fileName
   )
   var len = 0
   for blobPart in fileBits:
@@ -94,18 +86,16 @@ proc newWebFile(ctx: JSContext; fileBits: seq[string]; fileName: string;
 #TODO File, Blob constructors
 
 proc getSize*(this: Blob): uint64 =
-  if this.isfile:
-    return uint64(WebFile(this).path.getFileSize())
+  if this.fd.isSome:
+    var statbuf: Stat
+    if fstat(this.fd.get, statbuf) < 0:
+      return 0
+    return uint64(statbuf.st_size)
   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('/')
-  return this.path.afterLast('/', 2)
-
 #TODO lastModified
 
 proc addBlobModule*(ctx: JSContext) =
diff --git a/src/types/formdata.nim b/src/types/formdata.nim
index b353b814..ec3d8bbf 100644
--- a/src/types/formdata.nim
+++ b/src/types/formdata.nim
@@ -1,7 +1,7 @@
 import std/strutils
 
 import io/dynstream
-import io/filestream
+import io/posixstream
 import js/javascript
 import types/blob
 import utils/twtstr
@@ -47,12 +47,11 @@ proc calcLength*(this: FormData): int =
       # 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 += int(entry.value.getSize())
     result += "\r\n".len # header is always followed by \r\n
     result += "\r\n".len # value is always followed by \r\n
+  result += "--".len + this.boundary.len + "--\r\n".len
+  result += "\r\n".len
 
 proc getContentType*(this: FormData): string =
   return "multipart/form-data; boundary=" & this.boundary
@@ -61,25 +60,27 @@ proc writeEntry*(stream: DynStream; 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("Content-Disposition: form-data; name=\"" & name &
+      "\"\r\n\r\n")
     stream.write(entry.svalue)
+    stream.write("\r\n")
   else:
-    stream.write("Content-Disposition: form-data; name=\"" & name & "\";")
+    var buf = "Content-Disposition: form-data; name=\"" & name & "\";"
     let filename = percentEncode(entry.filename, {'"', '\r', '\n'})
-    stream.write(" filename=\"" & filename & "\"\r\n")
+    buf &= " 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 = newDynFileStream(WebFile(blob).path)
-      if fs != nil:
+    buf &= "Content-Type: " & ctype & "\r\n\r\n"
+    stream.write(buf)
+    if blob.fd.isSome:
+      let ps = newPosixStream(blob.fd.get)
+      if ps != nil:
         var buf {.noinit.}: array[4096, uint8]
         while true:
-          let n = fs.recvData(addr buf[0], 4096)
+          let n = ps.recvData(addr buf[0], 4096)
           if n == 0:
             break
           stream.sendDataLoop(addr buf[0], n)
@@ -88,4 +89,7 @@ proc writeEntry*(stream: DynStream; entry: FormDataEntry; boundary: string) =
     else:
       stream.sendDataLoop(blob.buffer, int(blob.size))
     stream.write("\r\n")
+
+proc writeEnd*(stream: DynStream; boundary: string) =
+  stream.write("--" & boundary & "--\r\n")
   stream.write("\r\n")