about summary refs log tree commit diff stats
path: root/adapter/protocol
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-10-01 21:09:22 +0200
committerbptato <nincsnevem662@gmail.com>2024-10-01 21:18:59 +0200
commitb0a511f900a2884c0d1bb55e3991c068ef3e37f2 (patch)
tree2ac5d77d3f62d844335d0dd00e840a8f5b38d726 /adapter/protocol
parentee113a5643ed32b7f6ed2522aed92ac700021097 (diff)
downloadchawan-b0a511f900a2884c0d1bb55e3991c068ef3e37f2.tar.gz
ftp: remove libcurl dependency
This splits out sftp into a separate binary that *does* depend on
libcurl. However, ftp now uses the same socket code as gopher.

ftps is dropped, because I've never even tested it. Maybe I'll add
it back when we have working OpenSSL bindings.

This is still "doing the easy part first", now I have no clue how to
handle sftp because my initial plan ("just use the sftp binary") doesn't
work - sftp batch mode doesn't accept passwords. libssh2 remains the
sole candidate, but that's what libcurl wraps anyway.
Diffstat (limited to 'adapter/protocol')
-rw-r--r--adapter/protocol/ftp.nim360
-rw-r--r--adapter/protocol/lcgi.nim34
-rw-r--r--adapter/protocol/sftp.nim318
3 files changed, 464 insertions, 248 deletions
diff --git a/adapter/protocol/ftp.nim b/adapter/protocol/ftp.nim
index f46f8aee..986380d9 100644
--- a/adapter/protocol/ftp.nim
+++ b/adapter/protocol/ftp.nim
@@ -1,94 +1,16 @@
 import std/options
 import std/os
+import std/posix
 import std/strutils
 
-import curl
-import curlerrors
-import curlwrap
+import lcgi
 import dirlist
 
 import utils/twtstr
 
-type FtpHandle = ref object
-  curl: CURL
-  buffer: string
-  dirmode: bool
-  base: string
-  path: string
-  statusline: bool
-
-proc printHeader(op: FtpHandle) =
-    if op.dirmode:
-      stdout.write("""Content-Type: text/html
-
-<HTML>
-<HEAD>
-<BASE HREF=""" & op.base & """>
-<TITLE>""" & op.path & """</TITLE>
-</HEAD>
-<BODY>
-<H1>Index of """ & htmlEscape(op.path) & """</H1>
-<PRE>
-""")
-    else:
-      stdout.write('\n')
-
-proc curlWriteHeader(p: cstring; size, nitems: csize_t; userdata: pointer):
-    csize_t {.cdecl.} =
-  var line = newString(nitems)
-  if nitems > 0:
-    copyMem(addr line[0], p, nitems)
-  let op = cast[FtpHandle](userdata)
-  if not op.statusline:
-    if line.startsWith("150") or line.startsWith("125"):
-      op.statusline = true
-      var status: clong
-      op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status)
-      stdout.write("Status: " & $status & "\n")
-      op.printHeader()
-      return nitems
-    elif line.startsWith("530"): # login incorrect
-      op.statusline = true
-      var status: clong
-      op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status)
-      # unauthorized (shim http)
-      stdout.write("""
-Status: 401
-Content-Type: text/html
-
-<HTML>
-<HEAD>
-<TITLE>Unauthorized</TITLE>
-</HEAD>
-<BODY>
-<PRE>
-""" & htmlEscape(line) & """
-</PRE>
-</BODY>
-</HTML>
-""")
-  return nitems
-
-# From the documentation: size is always 1.
-proc curlWriteBody(p: cstring; size, nmemb: csize_t; userdata: pointer):
-    csize_t {.cdecl.} =
-  let op = cast[FtpHandle](userdata)
-  if not op.statusline:
-    op.statusline = true
-    op.printHeader()
-  if nmemb > 0:
-    if op.dirmode:
-      let i = op.buffer.len
-      op.buffer.setLen(op.buffer.len + int(nmemb))
-      copyMem(addr op.buffer[i], p, nmemb)
-    else:
-      return csize_t(stdout.writeBuffer(p, int(nmemb)))
-  return nmemb
-
-proc finish(op: FtpHandle) =
-  let op = op
+proc finish(buffer: string) =
   var items: seq[DirlistItem] = @[]
-  for line in op.buffer.split('\n'):
+  for line in buffer.split('\n'):
     if line.len == 0: continue
     var i = 10 # permission
     template skip_till_space =
@@ -122,7 +44,10 @@ proc finish(op: FtpHandle) =
     skip_till_space # y
     let dates = line.substr(datestarti, i)
     inc i
-    let name = line.substr(i)
+    var j = line.len
+    if line[^1] == '\r':
+      dec j
+    let name = line.substr(i, j - 1)
     if name == "." or name == "..": continue
     case line[0]
     of 'l': # link
@@ -151,172 +76,133 @@ proc finish(op: FtpHandle) =
   stdout.write(makeDirlist(items))
   stdout.write("\n</PRE>\n</BODY>\n</HTML>\n")
 
-proc matchesPattern(s, pat: openArray[char]): bool =
-  var i = 0
-  for j, c in pat:
-    if c == '*':
-      while i < s.len:
-        if s.toOpenArray(i, s.high).matchesPattern(pat.toOpenArray(j + 1,
-            pat.high)):
-          return true
-        inc i
-      return false
-    if i >= s.len or c != '?' and c != s[i]:
-      return false
-    inc i
-  return true
+proc sendCommand(os, ps: PosixStream; cmd, param: string; outs: var string):
+    int32 =
+  if cmd != "":
+    if param == "":
+      ps.sendDataLoop(cmd & "\r\n")
+    else:
+      ps.sendDataLoop(cmd & ' ' & param & "\r\n")
+  var buf = newString(4)
+  try:
+    ps.recvDataLoop(buf)
+  except EOFError:
+    os.die("InvalidResponse")
+  if buf.len < 4:
+    os.die("InvalidResponse")
+  outs = ""
+  while (let c = ps.sreadChar(); c != '\n'):
+    outs &= c
+  let status = parseInt32(buf.toOpenArray(0, 2)).get(-1)
+  if buf[3] == ' ':
+    return status
+  buf[3] = ' '
+  while true: # multiline
+    var lbuf = ""
+    while (let c = ps.sreadChar(); c != '\n'):
+      lbuf &= c
+    outs &= lbuf
+    if lbuf.startsWith(buf):
+      break
+  return status
 
-proc matchesPattern(s: string; pats: openArray[string]): bool =
-  for pat in pats:
-    if s.matchesPattern(pat):
-      return true
-  return false
+proc sdie(os: PosixStream; status: int; s, obuf: string) {.noreturn.} =
+  os.sendDataLoop("Status: " & $status & "\nContent-Type: text/html\n\n" & """
+<h1>""" & s & """</h1>
 
-proc parseSSHConfig(f: File; curl: CURL; host: string; idSet: var bool) =
-  var skipTillNext = false
-  var line: string
-  var certificateFile = ""
-  var identityFile = ""
-  while f.readLine(line):
-    var i = line.skipBlanks(0)
-    if i == line.len or line[i] == '#':
-      continue
-    let k = line.until(AsciiWhitespace, i)
-    i = line.skipBlanks(i + k.len)
-    if i < line.len and line[i] == '=':
-      i = line.skipBlanks(i + 1)
-    if i == line.len or line[i] == '#':
-      continue
-    var args = newSeq[string]()
-    while i < line.len:
-      let isStr = line[i] in {'"', '\''}
-      if isStr:
-        inc i
-      var quot = false
-      var arg = ""
-      while i < line.len:
-        if not quot:
-          if line[i] == '\\':
-            quot = true
-            continue
-          elif line[i] == '"' and isStr or line[i] == ' ' and not isStr:
-            inc i
-            break
-        quot = false
-        arg &= line[i]
-        inc i
-      if arg.len > 0:
-        args.add(arg)
-    if k == "Match": #TODO support this
-      skipTillNext = true
-    elif k == "Host":
-      skipTillNext = not host.matchesPattern(args)
-    elif skipTillNext:
-      continue
-    elif k == "IdentityFile":
-      if args.len != 1:
-        continue # error
-      identityFile = expandTilde(args[0])
-    elif k == "CertificateFile":
-      if args.len != 1:
-        continue # error
-      certificateFile = expandTilde(args[0])
-  if identityFile != "":
-    curl.setopt(CURLOPT_SSH_PRIVATE_KEYFILE, identityFile)
-    idSet = true
-  if certificateFile != "":
-    curl.setopt(CURLOPT_SSH_PUBLIC_KEYFILE, certificateFile)
-  f.close()
+The server has returned the following message:
+
+<plaintext>
+""" & obuf)
+
+const Success = 200 .. 299
+proc passiveMode(os, ps: PosixStream; host: string; ipv6: bool): PosixStream =
+  var obuf = ""
+  if ipv6:
+    if os.sendCommand(ps, "EPSV", "", obuf) != 229:
+      os.die("InvalidResponse")
+    var i = obuf.find('(')
+    if i == -1:
+      os.die("InvalidResponse")
+    i += 4 # skip delims
+    let j = obuf.find(')', i)
+    if j == -1:
+      os.die("InvalidResponse")
+    let port = obuf.substr(i, j - 2)
+    return os.connectSocket(host, port)
+  if os.sendCommand(ps, "PASV", "", obuf) notin Success:
+    os.sdie(500, "Couldn't enter passive mode", obuf)
+  let i = obuf.find(AsciiDigit)
+  if i == -1:
+    os.die("InvalidResponse")
+  var j = obuf.find(AllChars - AsciiDigit - {','}, i)
+  if j == -1:
+    j = obuf.len
+  let ss = obuf.substr(i, j - 1).split(',')
+  if ss.len < 6:
+    os.die("InvalidResponse")
+  var ipv4 = ss[0]
+  for x in ss.toOpenArray(1, 3):
+    ipv4 &= '.'
+    ipv4 &= x
+  let x = parseUInt16(ss[4])
+  let y = parseUInt16(ss[5])
+  if x.isNone or y.isNone:
+    os.die("InvalidResponse")
+  let port = $((x.get shl 8) or y.get)
+  return os.connectSocket(host, port)
 
 proc main() =
-  let curl = curl_easy_init()
-  doAssert curl != nil
+  let os = newPosixStream(STDOUT_FILENO)
   var opath = getEnv("MAPPED_URI_PATH")
   if opath == "":
     opath = "/"
   let path = percentDecode(opath)
-  let op = FtpHandle(
-    curl: curl,
-    dirmode: path.len > 0 and path[^1] == '/'
-  )
-  let url = curl_url()
-  const flags = cuint(CURLU_PATH_AS_IS)
-  let scheme = getEnv("MAPPED_URI_SCHEME")
-  url.set(CURLUPART_SCHEME, scheme, flags)
-  let username = getEnv("MAPPED_URI_USERNAME")
-  if username != "":
-    url.set(CURLUPART_USER, username, flags)
+  let dirmode = path.len > 0 and path[^1] == '/'
   let host = getEnv("MAPPED_URI_HOST")
+  let username = getEnv("MAPPED_URI_USERNAME")
   let password = getEnv("MAPPED_URI_PASSWORD")
-  var idSet = false
-  # Parse SSH config for sftp.
-  if scheme == "sftp":
-    let systemConfig = "/etc/ssh/ssh_config"
-    if fileExists(systemConfig):
-      var f: File
-      if f.open(systemConfig):
-        parseSSHConfig(f, curl, host, idSet)
-    let userConfig = expandTilde("~/.ssh/config")
-    if fileExists(userConfig):
-      var f: File
-      if f.open(userConfig):
-        parseSSHConfig(f, curl, host, idSet)
-    if idSet:
-      curl.setopt(CURLOPT_KEYPASSWD, password)
-  url.set(CURLUPART_PASSWORD, password, flags)
-  url.set(CURLUPART_HOST, host, flags)
-  let port = getEnv("MAPPED_URI_PORT")
-  if port != "":
-    url.set(CURLUPART_PORT, port, flags)
-  # By default, cURL CWD's into relative paths, and an extra slash is
-  # necessary to specify absolute paths.
-  # This is incredibly confusing, and probably not what the user wanted.
-  # So we work around it by adding the extra slash ourselves.
-  #
-  # But before that, we take the serialized URL without the path for
-  # setting the base URL:
-  url.set(CURLUPART_PATH, opath, flags)
-  if op.dirmode:
-    let surl = url.get(CURLUPART_URL, cuint(CURLU_PUNY2IDN))
-    if surl == nil:
-      stdout.write("Cha-Control: ConnectionError InvalidURL\n")
-      curl_url_cleanup(url)
-      curl_easy_cleanup(curl)
-      return
-    op.base = $surl
-    op.path = path
-    curl_free(surl)
-  # Now for the workaround:
-  if scheme != "sftp" and (opath.len <= 1 or opath[1] != '~'):
-    url.set(CURLUPART_PATH, '/' & opath, flags)
-  # Another hack: if password was set for the identity file, then clear it from
-  # the URL.
-  if idSet:
-    url.set(CURLUPART_PASSWORD, nil, flags)
-  # Set opts for the request
-  curl.setopt(CURLOPT_CURLU, url)
-  curl.setopt(CURLOPT_HEADERDATA, op)
-  curl.setopt(CURLOPT_HEADERFUNCTION, curlWriteHeader)
-  curl.setopt(CURLOPT_WRITEDATA, op)
-  curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody)
-  curl.setopt(CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_SINGLECWD)
-  let purl = getEnv("ALL_PROXY")
-  if purl != "":
-    curl.setopt(CURLOPT_PROXY, purl)
+  var port = getEnv("MAPPED_URI_PORT")
+  if port == "":
+    port = "21"
   if getEnv("REQUEST_METHOD") != "GET":
     # fail
-    stdout.write("Cha-Control: ConnectionError InvalidMethod\n")
+    os.die("InvalidMethod")
+  var ipv6: bool
+  let ps = os.connectSocket(host, port, ipv6)
+  var obuf = ""
+  if os.sendCommand(ps, "", "", obuf) != 220:
+    let s = obuf.deleteChars({'\n', '\r'})
+    os.die("ConnectionRefused " & s)
+  var ustatus = os.sendCommand(ps, "USER", username, obuf)
+  if ustatus == 331:
+    ustatus = os.sendCommand(ps, "PASS", password, obuf)
+  if ustatus in Success:
+    discard # no need for pass
+  else:
+    os.sdie(401, "Unauthorized", obuf)
+  discard os.sendCommand(ps, "TYPE", "I", obuf) # request raw data
+  let passive = os.passiveMode(ps, host, ipv6)
+  if dirmode:
+    if os.sendCommand(ps, "LIST", "", obuf) == 550:
+      os.sdie(404, "Not found", obuf)
+    os.sendDataLoop("""Content-Type: text/html
+
+<HTML>
+<BODY>
+<H1>Index of """ & htmlEscape(path) & """</H1>
+<PRE>""")
+    let buffer = passive.recvAll()
+    finish(buffer)
   else:
-    let res = curl_easy_perform(curl)
-    if res != CURLE_OK:
-      if not op.statusline:
-        if res == CURLE_LOGIN_DENIED:
-          stdout.write("Status: 401\n")
-        else:
-          stdout.write(getCurlConnectionError(res))
-    elif op.dirmode:
-      op.finish()
-  curl_url_cleanup(url)
-  curl_easy_cleanup(curl)
+    if os.sendCommand(ps, "RETR", path, obuf) == 550:
+      os.sdie(404, "Not found", obuf)
+    os.sendDataLoop("\n")
+    var buffer {.noinit.}: array[4096, uint8]
+    while true:
+      let n = passive.recvData(buffer)
+      if n == 0:
+        break
+      os.sendDataLoop(buffer.toOpenArray(0, n - 1))
 
 main()
diff --git a/adapter/protocol/lcgi.nim b/adapter/protocol/lcgi.nim
index 03926d4a..e33f0609 100644
--- a/adapter/protocol/lcgi.nim
+++ b/adapter/protocol/lcgi.nim
@@ -9,12 +9,14 @@ import utils/twtstr
 export dynstream
 export twtstr
 
+export STDIN_FILENO, STDOUT_FILENO
+
 proc die*(os: PosixStream; s: string) =
   os.sendDataLoop("Cha-Control: ConnectionError " & s)
   quit(1)
 
 proc openSocket(os: PosixStream; host, port, resFail, connFail: string;
-    res: var ptr AddrInfo): SocketHandle =
+    res: var ptr AddrInfo; outIpv6: var bool): SocketHandle =
   var err: cint
   for family in [AF_INET, AF_INET6, AF_UNSPEC]:
     var hints = AddrInfo(
@@ -28,19 +30,20 @@ proc openSocket(os: PosixStream; host, port, resFail, connFail: string;
   if err < 0:
     os.die(resFail & ' ' & $gai_strerror(err))
   let sock = socket(res.ai_family, res.ai_socktype, res.ai_protocol)
-  freeaddrinfo(res)
   if cint(sock) < 0:
     os.die("InternalError could not open socket")
   return sock
 
-proc connectSocket(os: PosixStream; host, port, resFail, connFail: string):
-    PosixStream =
+proc connectSocket(os: PosixStream; host, port, resFail, connFail: string;
+    outIpv6: var bool): PosixStream =
   var res: ptr AddrInfo
-  let sock = os.openSocket(host, port, resFail, connFail, res)
+  let sock = os.openSocket(host, port, resFail, connFail, res, outIpv6)
   let ps = newPosixStream(sock)
   if connect(sock, res.ai_addr, res.ai_addrlen) < 0:
     ps.sclose()
     os.die(connFail)
+  outIpv6 = res.ai_family == AF_INET6
+  freeaddrinfo(res)
   return ps
 
 proc authenticateSocks5(os, ps: PosixStream; buf: array[2, uint8];
@@ -101,8 +104,9 @@ proc sendSocks5Domain(os, ps: PosixStream; host, port: string) =
 
 proc connectSocks5Socket(os: PosixStream; host, port, proxyHost, proxyPort,
     proxyUser, proxyPass: string): PosixStream =
+  var dummy = false
   let ps = os.connectSocket(proxyHost, proxyPort, "FailedToResolveProxy",
-    "ProxyRefusedToConnect")
+    "ProxyRefusedToConnect", dummy)
   const NoAuth = "\x05\x01\x00"
   const WithAuth = "\x05\x02\x00\x02"
   ps.sendDataLoop(if proxyUser != "": NoAuth else: WithAuth)
@@ -112,8 +116,8 @@ proc connectSocks5Socket(os: PosixStream; host, port, proxyHost, proxyPort,
   os.sendSocks5Domain(ps, host, port)
   return ps
 
-proc connectProxySocket(os: PosixStream; host, port, proxy: string):
-    PosixStream =
+proc connectProxySocket(os: PosixStream; host, port, proxy: string;
+    outIpv6: var bool): PosixStream =
   let scheme = proxy.until(':')
   # We always use socks5h, actually.
   if scheme != "socks5" and scheme != "socks5h":
@@ -141,9 +145,17 @@ proc connectProxySocket(os: PosixStream; host, port, proxy: string):
   let proxyPort = proxy.substr(i)
   return os.connectSocks5Socket(host, port, proxyHost, proxyPort, user, pass)
 
-proc connectSocket*(os: PosixStream; host, port: string): PosixStream =
+# Note: outIpv6 is not read; it just indicates whether the socket's
+# address is IPv6.
+# In case we connect to a proxy, only the target matters.
+proc connectSocket*(os: PosixStream; host, port: string; outIpv6: var bool):
+    PosixStream =
   let proxy = getEnv("ALL_PROXY")
   if proxy != "":
-    return os.connectProxySocket(host, port, proxy)
+    return os.connectProxySocket(host, port, proxy, outIpv6)
   return os.connectSocket(host, port, "FailedToResolveHost",
-    "ConnectionRefused")
+    "ConnectionRefused", outIpv6)
+
+proc connectSocket*(os: PosixStream; host, port: string): PosixStream =
+  var dummy = false
+  return os.connectSocket(host, port, dummy)
diff --git a/adapter/protocol/sftp.nim b/adapter/protocol/sftp.nim
new file mode 100644
index 00000000..a7dae913
--- /dev/null
+++ b/adapter/protocol/sftp.nim
@@ -0,0 +1,318 @@
+import std/options
+import std/os
+import std/strutils
+
+import curl
+import curlerrors
+import curlwrap
+import dirlist
+
+import utils/twtstr
+
+type FtpHandle = ref object
+  curl: CURL
+  buffer: string
+  dirmode: bool
+  base: string
+  path: string
+  statusline: bool
+
+proc printHeader(op: FtpHandle) =
+    if op.dirmode:
+      stdout.write("""Content-Type: text/html
+
+<HTML>
+<HEAD>
+<BASE HREF=""" & op.base & """>
+<TITLE>""" & op.path & """</TITLE>
+</HEAD>
+<BODY>
+<H1>Index of """ & htmlEscape(op.path) & """</H1>
+<PRE>
+""")
+    else:
+      stdout.write('\n')
+
+proc curlWriteHeader(p: cstring; size, nitems: csize_t; userdata: pointer):
+    csize_t {.cdecl.} =
+  var line = newString(nitems)
+  if nitems > 0:
+    copyMem(addr line[0], p, nitems)
+  let op = cast[FtpHandle](userdata)
+  if not op.statusline:
+    if line.startsWith("150") or line.startsWith("125"):
+      op.statusline = true
+      var status: clong
+      op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status)
+      stdout.write("Status: " & $status & "\n")
+      op.printHeader()
+      return nitems
+    elif line.startsWith("530"): # login incorrect
+      op.statusline = true
+      var status: clong
+      op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status)
+      # unauthorized (shim http)
+      stdout.write("""
+Status: 401
+Content-Type: text/html
+
+<HTML>
+<HEAD>
+<TITLE>Unauthorized</TITLE>
+</HEAD>
+<BODY>
+<PRE>
+""" & htmlEscape(line) & """
+</PRE>
+</BODY>
+</HTML>
+""")
+  return nitems
+
+# From the documentation: size is always 1.
+proc curlWriteBody(p: cstring; size, nmemb: csize_t; userdata: pointer):
+    csize_t {.cdecl.} =
+  let op = cast[FtpHandle](userdata)
+  if not op.statusline:
+    op.statusline = true
+    op.printHeader()
+  if nmemb > 0:
+    if op.dirmode:
+      let i = op.buffer.len
+      op.buffer.setLen(op.buffer.len + int(nmemb))
+      copyMem(addr op.buffer[i], p, nmemb)
+    else:
+      return csize_t(stdout.writeBuffer(p, int(nmemb)))
+  return nmemb
+
+proc finish(op: FtpHandle) =
+  let op = op
+  var items: seq[DirlistItem] = @[]
+  for line in op.buffer.split('\n'):
+    if line.len == 0: continue
+    var i = 10 # permission
+    template skip_till_space =
+      while i < line.len and line[i] != ' ':
+        inc i
+    # link count
+    i = line.skipBlanks(i)
+    while i < line.len and line[i] in AsciiDigit:
+      inc i
+    # owner
+    i = line.skipBlanks(i)
+    skip_till_space
+    # group
+    i = line.skipBlanks(i)
+    while i < line.len and line[i] != ' ':
+      inc i
+    # size
+    i = line.skipBlanks(i)
+    var sizes = ""
+    while i < line.len and line[i] in AsciiDigit:
+      sizes &= line[i]
+      inc i
+    let nsize = parseInt64(sizes).get(-1)
+    # date
+    i = line.skipBlanks(i)
+    let datestarti = i
+    skip_till_space # m
+    i = line.skipBlanks(i)
+    skip_till_space # d
+    i = line.skipBlanks(i)
+    skip_till_space # y
+    let dates = line.substr(datestarti, i)
+    inc i
+    let name = line.substr(i)
+    if name == "." or name == "..": continue
+    case line[0]
+    of 'l': # link
+      let linki = name.find(" -> ")
+      let linkfrom = name.substr(0, linki - 1)
+      let linkto = name.substr(linki + 4) # you?
+      items.add(DirlistItem(
+        t: ditLink,
+        name: linkfrom,
+        modified: dates,
+        linkto: linkto
+      ))
+    of 'd': # directory
+      items.add(DirlistItem(
+        t: ditDir,
+        name: name,
+        modified: dates
+      ))
+    else: # file
+      items.add(DirlistItem(
+        t: ditFile,
+        name: name,
+        modified: dates,
+        nsize: int(nsize)
+      ))
+  stdout.write(makeDirlist(items))
+  stdout.write("\n</PRE>\n</BODY>\n</HTML>\n")
+
+proc matchesPattern(s, pat: openArray[char]): bool =
+  var i = 0
+  for j, c in pat:
+    if c == '*':
+      while i < s.len:
+        if s.toOpenArray(i, s.high).matchesPattern(pat.toOpenArray(j + 1,
+            pat.high)):
+          return true
+        inc i
+      return false
+    if i >= s.len or c != '?' and c != s[i]:
+      return false
+    inc i
+  return true
+
+proc matchesPattern(s: string; pats: openArray[string]): bool =
+  for pat in pats:
+    if s.matchesPattern(pat):
+      return true
+  return false
+
+proc parseSSHConfig(f: File; curl: CURL; host: string; idSet: var bool) =
+  var skipTillNext = false
+  var line: string
+  var certificateFile = ""
+  var identityFile = ""
+  while f.readLine(line):
+    var i = line.skipBlanks(0)
+    if i == line.len or line[i] == '#':
+      continue
+    let k = line.until(AsciiWhitespace, i)
+    i = line.skipBlanks(i + k.len)
+    if i < line.len and line[i] == '=':
+      i = line.skipBlanks(i + 1)
+    if i == line.len or line[i] == '#':
+      continue
+    var args = newSeq[string]()
+    while i < line.len:
+      let isStr = line[i] in {'"', '\''}
+      if isStr:
+        inc i
+      var quot = false
+      var arg = ""
+      while i < line.len:
+        if not quot:
+          if line[i] == '\\':
+            quot = true
+            continue
+          elif line[i] == '"' and isStr or line[i] == ' ' and not isStr:
+            inc i
+            break
+        quot = false
+        arg &= line[i]
+        inc i
+      if arg.len > 0:
+        args.add(arg)
+    if k == "Match": #TODO support this
+      skipTillNext = true
+    elif k == "Host":
+      skipTillNext = not host.matchesPattern(args)
+    elif skipTillNext:
+      continue
+    elif k == "IdentityFile":
+      if args.len != 1:
+        continue # error
+      identityFile = expandTilde(args[0])
+    elif k == "CertificateFile":
+      if args.len != 1:
+        continue # error
+      certificateFile = expandTilde(args[0])
+  if identityFile != "":
+    curl.setopt(CURLOPT_SSH_PRIVATE_KEYFILE, identityFile)
+    idSet = true
+  if certificateFile != "":
+    curl.setopt(CURLOPT_SSH_PUBLIC_KEYFILE, certificateFile)
+  f.close()
+
+proc main() =
+  let curl = curl_easy_init()
+  doAssert curl != nil
+  var opath = getEnv("MAPPED_URI_PATH")
+  if opath == "":
+    opath = "/"
+  let path = percentDecode(opath)
+  let op = FtpHandle(
+    curl: curl,
+    dirmode: path.len > 0 and path[^1] == '/'
+  )
+  let url = curl_url()
+  const flags = cuint(CURLU_PATH_AS_IS)
+  let scheme = getEnv("MAPPED_URI_SCHEME")
+  url.set(CURLUPART_SCHEME, scheme, flags)
+  let username = getEnv("MAPPED_URI_USERNAME")
+  if username != "":
+    url.set(CURLUPART_USER, username, flags)
+  let host = getEnv("MAPPED_URI_HOST")
+  let password = getEnv("MAPPED_URI_PASSWORD")
+  var idSet = false
+  # Parse SSH config for sftp.
+  let systemConfig = "/etc/ssh/ssh_config"
+  if fileExists(systemConfig):
+    var f: File
+    if f.open(systemConfig):
+      parseSSHConfig(f, curl, host, idSet)
+  let userConfig = expandTilde("~/.ssh/config")
+  if fileExists(userConfig):
+    var f: File
+    if f.open(userConfig):
+      parseSSHConfig(f, curl, host, idSet)
+  if idSet:
+    curl.setopt(CURLOPT_KEYPASSWD, password)
+  url.set(CURLUPART_PASSWORD, password, flags)
+  url.set(CURLUPART_HOST, host, flags)
+  let port = getEnv("MAPPED_URI_PORT")
+  if port != "":
+    url.set(CURLUPART_PORT, port, flags)
+  # By default, cURL CWD's into relative paths, and an extra slash is
+  # necessary to specify absolute paths.
+  # This is incredibly confusing, and probably not what the user wanted.
+  # So we work around it by adding the extra slash ourselves.
+  #
+  # But before that, we take the serialized URL without the path for
+  # setting the base URL:
+  url.set(CURLUPART_PATH, opath, flags)
+  if op.dirmode:
+    let surl = url.get(CURLUPART_URL, cuint(CURLU_PUNY2IDN))
+    if surl == nil:
+      stdout.write("Cha-Control: ConnectionError InvalidURL\n")
+      curl_url_cleanup(url)
+      curl_easy_cleanup(curl)
+      return
+    op.base = $surl
+    op.path = path
+    curl_free(surl)
+  # Another hack: if password was set for the identity file, then clear it from
+  # the URL.
+  if idSet:
+    url.set(CURLUPART_PASSWORD, nil, flags)
+  # Set opts for the request
+  curl.setopt(CURLOPT_CURLU, url)
+  curl.setopt(CURLOPT_HEADERDATA, op)
+  curl.setopt(CURLOPT_HEADERFUNCTION, curlWriteHeader)
+  curl.setopt(CURLOPT_WRITEDATA, op)
+  curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody)
+  curl.setopt(CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_SINGLECWD)
+  let purl = getEnv("ALL_PROXY")
+  if purl != "":
+    curl.setopt(CURLOPT_PROXY, purl)
+  if getEnv("REQUEST_METHOD") != "GET":
+    # fail
+    stdout.write("Cha-Control: ConnectionError InvalidMethod\n")
+  else:
+    let res = curl_easy_perform(curl)
+    if res != CURLE_OK:
+      if not op.statusline:
+        if res == CURLE_LOGIN_DENIED:
+          stdout.write("Status: 401\n")
+        else:
+          stdout.write(getCurlConnectionError(res))
+    elif op.dirmode:
+      op.finish()
+  curl_url_cleanup(url)
+  curl_easy_cleanup(curl)
+
+main()