about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-09-28 17:56:45 +0200
committerbptato <nincsnevem662@gmail.com>2024-09-28 17:56:45 +0200
commit1dea3e9fbe4a902db6325195df0d7a465f82cfc5 (patch)
treed400bcaa2fdf4c71a81919a45c0a58a345bbc8fc
parent6a0e957e1f2c9f5bea0882efbf2e0494cd5074fa (diff)
downloadchawan-1dea3e9fbe4a902db6325195df0d7a465f82cfc5.tar.gz
gopher: do not depend on libcurl
I'm thinking of making libcurl entirely optional; let's start with the
easiest part.

I've added a SOCKS5 client for ALL_PROXY support; I know curl supported
others too, but whatever.
-rw-r--r--Makefile5
-rw-r--r--adapter/protocol/gopher.nim127
-rw-r--r--adapter/protocol/lcgi.nim149
-rw-r--r--doc/protocols.md4
-rw-r--r--res/urimethodmap1
-rw-r--r--src/io/dynstream.nim8
-rw-r--r--src/loader/connecterror.nim6
7 files changed, 219 insertions, 81 deletions
diff --git a/Makefile b/Makefile
index ce63385c..c9d0c248 100644
--- a/Makefile
+++ b/Makefile
@@ -93,6 +93,7 @@ $(OUTDIR_CGI_BIN)/gmifetch: adapter/protocol/gmifetch.c
 
 twtstr = src/utils/twtstr.nim src/utils/charcategory.nim src/utils/map.nim src/utils/twtuni.nim
 dynstream = src/io/dynstream.nim src/io/serversocket.nim
+lcgi = $(dynstream) $(twtstr) adapter/protocol/lcgi.nim
 $(OUTDIR_CGI_BIN)/man: lib/monoucha/monoucha/jsregex.nim \
 		lib/monoucha/monoucha/libregexp.nim src/types/opt.nim $(twtstr)
 $(OUTDIR_CGI_BIN)/http: adapter/protocol/curlwrap.nim \
@@ -102,9 +103,7 @@ $(OUTDIR_CGI_BIN)/about: res/chawan.html res/license.md
 $(OUTDIR_CGI_BIN)/file: adapter/protocol/dirlist.nim $(twtstr) src/utils/strwidth.nim
 $(OUTDIR_CGI_BIN)/ftp: adapter/protocol/dirlist.nim $(twtstr) src/utils/strwidth.nim \
 		src/types/opt.nim adapter/protocol/curl.nim
-$(OUTDIR_CGI_BIN)/gopher: adapter/protocol/curlwrap.nim adapter/protocol/curlerrors.nim \
-		adapter/gophertypes.nim adapter/protocol/curl.nim \
-		$(twtstr)
+$(OUTDIR_CGI_BIN)/gopher: adapter/gophertypes.nim $(lcgi)
 $(OUTDIR_CGI_BIN)/stbi: adapter/img/stbi.nim adapter/img/stb_image.c \
 		adapter/img/stb_image.h src/utils/sandbox.nim $(dynstream)
 $(OUTDIR_CGI_BIN)/jebp: adapter/img/jebp.c adapter/img/jebp.h \
diff --git a/adapter/protocol/gopher.nim b/adapter/protocol/gopher.nim
index 13ade18c..b97ced2b 100644
--- a/adapter/protocol/gopher.nim
+++ b/adapter/protocol/gopher.nim
@@ -1,33 +1,13 @@
-when NimMajor >= 2:
-  import std/envvars
-else:
-  import std/os
-
-import curl
-import curlerrors
-import curlwrap
+import std/options
+import std/os
+import std/posix
+import std/strutils
 
 import ../gophertypes
+import lcgi
 
-import utils/twtstr
-
-type GopherHandle = ref object
-  curl: CURL
-  t: GopherType
-  statusline: bool
-
-proc onStatusLine(op: GopherHandle) =
-  let s = case op.t
-  of gtDirectory, gtSearch: "Content-Type: text/gopher\n"
-  of gtHTML: "Content-Type: text/html\n"
-  of gtGif: "Content-Type: image/gif\n"
-  of gtPng: "Content-Type: image/png\n"
-  of gtTextFile, gtError: "Content-Type: text/plain\n"
-  else: ""
-  stdout.write(s & "\n")
-
-proc loadSearch(op: GopherHandle; surl: string) =
-  stdout.write("""
+proc loadSearch(os: PosixStream; t: GopherType; surl: string) =
+  os.sendDataLoop("""
 Content-Type: text/html
 
 <!DOCTYPE HTML>
@@ -44,58 +24,57 @@ Content-Type: text/html
 </HTML>
 """)
 
-# From the documentation: size is always 1.
-proc curlWriteBody(p: cstring; size, nmemb: csize_t; userdata: pointer):
-    csize_t {.cdecl.} =
-  let op = cast[GopherHandle](userdata)
-  if not op.statusline:
-    op.statusline = true
-    op.onStatusLine()
-  return csize_t(stdout.writeBuffer(p, int(nmemb)))
+proc loadRegular(os: PosixStream; t: GopherType; path: var string;
+    host, port, query: string) =
+  let ps = os.connectSocket(host, port)
+  if query != "":
+    path &= '\t'
+    path &= query
+  path &= '\n'
+  ps.sendDataLoop(percentDecode(path))
+  let s = case t
+  of gtDirectory, gtSearch: "Content-Type: text/gopher\n"
+  of gtHTML: "Content-Type: text/html\n"
+  of gtGif: "Content-Type: image/gif\n"
+  of gtPng: "Content-Type: image/png\n"
+  of gtTextFile, gtError: "Content-Type: text/plain\n"
+  else: ""
+  os.sendDataLoop(s & '\n')
+  var buffer: array[4096, uint8]
+  while true:
+    let n = ps.recvData(buffer)
+    if n == 0:
+      break
+    os.sendDataLoop(addr buffer[0], n)
+  ps.sclose()
 
 proc main() =
-  let curl = curl_easy_init()
-  doAssert curl != nil
+  let os = newPosixStream(STDOUT_FILENO)
   if getEnv("REQUEST_METHOD") != "GET":
-    stdout.write("Cha-Control: ConnectionError InvalidMethod")
-    return
+    os.die("InvalidMethod")
+  let scheme = getEnv("MAPPED_URI_SCHEME")
+  var host = getEnv("MAPPED_URI_HOST")
+  if host == "":
+    os.die("InvalidURL missing hostname")
+  if host[0] == '[' and host[^1] == ']':
+    host.delete(0..0)
+    host.setLen(host.high)
+  let port = $parseInt32(getEnv("MAPPED_URI_PORT")).get(70)
+  let query = getEnv("MAPPED_URI_QUERY").after('=')
   var path = getEnv("MAPPED_URI_PATH")
-  if path.len < 1:
-    path &= '/'
-  if path.len < 2:
-    path &= '1'
-  let url = curl_url()
-  const flags = cuint(CURLU_PATH_AS_IS)
-  url.set(CURLUPART_SCHEME, getEnv("MAPPED_URI_SCHEME"), flags)
-  url.set(CURLUPART_HOST, getEnv("MAPPED_URI_HOST"), flags)
-  let port = getEnv("MAPPED_URI_PORT")
-  if port != "":
-    url.set(CURLUPART_PORT, port, flags)
-  url.set(CURLUPART_PATH, path, flags)
-  let query = getEnv("MAPPED_URI_QUERY")
-  if query != "":
-    url.set(CURLUPART_QUERY, query.after('='), flags)
-  let op = GopherHandle(
-    curl: curl,
-    t: gopherType(path[1])
-  )
-  if op.t == gtSearch and query == "":
-    const flags = cuint(CURLU_PUNY2IDN)
-    let surl = url.get(CURLUPART_URL, flags)
-    if surl == nil:
-      stdout.write("Cha-Control: ConnectionError InvalidURL")
+  var i = 0
+  while i < path.len and path[i] == '/':
+    inc i
+  var t = gtDirectory
+  if i < path.len:
+    t = gopherType(path[i])
+    if t != gtUnknown:
+      path.delete(0 .. i)
     else:
-      op.loadSearch($surl)
+      t = gtDirectory
+  if t == gtSearch and query == "":
+    os.loadSearch(t, scheme & "://" & host & ":" & port & '/')
   else:
-    curl.setopt(CURLOPT_CURLU, url)
-    curl.setopt(CURLOPT_WRITEDATA, op)
-    curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody)
-    let proxy = getEnv("ALL_PROXY")
-    if proxy != "":
-      curl.setopt(CURLOPT_PROXY, proxy)
-    let res = curl_easy_perform(curl)
-    if res != CURLE_OK and not op.statusline:
-      stdout.write(getCurlConnectionError(res))
-  curl_easy_cleanup(curl)
+    os.loadRegular(t, path, host, port, query)
 
 main()
diff --git a/adapter/protocol/lcgi.nim b/adapter/protocol/lcgi.nim
new file mode 100644
index 00000000..9c0bdc16
--- /dev/null
+++ b/adapter/protocol/lcgi.nim
@@ -0,0 +1,149 @@
+import std/options
+import std/os
+import std/posix
+import std/strutils
+
+import io/dynstream
+import utils/twtstr
+
+export dynstream
+export twtstr
+
+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 =
+  var err: cint
+  for family in [AF_INET, AF_INET6, AF_UNSPEC]:
+    var hints = AddrInfo(
+      ai_family: family,
+      ai_socktype: SOCK_STREAM,
+      ai_protocol: IPPROTO_TCP
+    )
+    err = getaddrinfo(cstring(host), cstring(port), addr hints, res)
+    if err == 0:
+      break
+  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 =
+  var res: ptr AddrInfo
+  let sock = os.openSocket(host, port, resFail, connFail, res)
+  let ps = newPosixStream(sock)
+  if connect(sock, res.ai_addr, res.ai_addrlen) < 0:
+    ps.sclose()
+    os.die(connFail)
+  return ps
+
+proc authenticateSocks5(os, ps: PosixStream; buf: array[2, uint8];
+    user, pass: string) =
+  if buf[0] != 5:
+    os.die("ProxyInvalidResponse wrong socks version")
+  case buf[1]
+  of 0x00:
+    discard # no auth
+  of 0x02:
+    if user.len > 255 or pass.len > 255:
+      os.die("InternalError username or password too long")
+    let sbuf = "\x01" & char(user.len) & user & char(pass.len) & pass
+    ps.sendDataLoop(sbuf)
+    var rbuf = default(array[2, uint8])
+    ps.recvDataLoop(rbuf)
+    if rbuf[0] != 1:
+      os.die("ProxyInvalidResponse wrong auth version")
+    if rbuf[1] != 0:
+      os.die("ProxyAuthFail")
+  of 0xFF:
+    os.die("ProxyAuthFail proxy doesn't support our auth")
+  else:
+    os.die("ProxyInvalidResponse received wrong auth method " & $buf[1])
+
+proc sendSocks5Domain(os, ps: PosixStream; host, port: string) =
+  if host.len > 255:
+    os.die("InternalError host too long to send to proxy")
+  let dstaddr = "\x03" & char(host.len) & host
+  let x = parseUInt16(port)
+  if x.isNone:
+    os.die("InternalError wrong port")
+  let port = x.get
+  let sbuf = "\x05\x01\x00" & dstaddr & char(port shr 8) & char(port and 0xFF)
+  ps.sendDataLoop(sbuf)
+  var rbuf = default(array[4, uint8])
+  ps.recvDataLoop(rbuf)
+  if rbuf[0] != 5:
+    os.die("ProxyInvalidResponse")
+  if rbuf[1] != 0:
+    os.die("ProxyRefusedToConnect")
+  case rbuf[3]
+  of 0x01:
+    var ipv4 = default(array[4, uint8])
+    ps.recvDataLoop(ipv4)
+  of 0x03:
+    var len = [0u8]
+    ps.recvDataLoop(len)
+    var domain = newString(int(len[0]))
+    ps.recvDataLoop(domain)
+  of 0x04:
+    var ipv6 = default(array[16, uint8])
+    ps.recvDataLoop(ipv6)
+  else:
+    os.die("ProxyInvalidResponse")
+  var bndport = default(array[2, uint8])
+  ps.recvDataLoop(bndport)
+
+proc connectSocks5Socket(os: PosixStream; host, port, proxyHost, proxyPort,
+    proxyUser, proxyPass: string): PosixStream =
+  let ps = os.connectSocket(proxyHost, proxyPort, "FailedToResolveProxy",
+    "ProxyRefusedToConnect")
+  const NoAuth = "\x05\x01\x00"
+  const WithAuth = "\x05\x02\x00\x02"
+  ps.sendDataLoop(if proxyUser != "": NoAuth else: WithAuth)
+  var buf = default(array[2, uint8])
+  ps.recvDataLoop(buf)
+  os.authenticateSocks5(ps, buf, proxyUser, proxyPass)
+  os.sendSocks5Domain(ps, host, port)
+  return ps
+
+proc connectProxySocket(os: PosixStream; host, port, proxy: string):
+    PosixStream =
+  let scheme = proxy.until(':')
+  # We always use socks5h, actually.
+  if scheme != "socks5" and scheme != "socks5h":
+    os.die("Only socks5 proxy is supported")
+  var i = scheme.len + 1
+  while i < proxy.len and proxy[i] == '/':
+    inc i
+  let authi = proxy.find('@', i)
+  var user = ""
+  var pass = ""
+  if authi != -1:
+    let auth = proxy.substr(i, authi - 1)
+    user = auth.until(':')
+    pass = auth.after(':')
+    i = authi + 1
+  var proxyHost = ""
+  while i < proxy.len:
+    let c = proxy[i]
+    if c == ':':
+      inc i
+      break
+    if c != '/':
+      proxyHost &= c
+    inc i
+  let proxyPort = proxy.substr(i)
+  return os.connectSocks5Socket(host, port, proxyHost, proxyPort, user, pass)
+
+proc connectSocket*(os: PosixStream; host, port: string): PosixStream =
+  let proxy = getEnv("ALL_PROXY")
+  if proxy != "":
+    return os.connectProxySocket(host, port, proxy)
+  return os.connectSocket(host, port, "FailedToResolveHost",
+    "ConnectionRefused")
diff --git a/doc/protocols.md b/doc/protocols.md
index f0f01b65..0d670797 100644
--- a/doc/protocols.md
+++ b/doc/protocols.md
@@ -63,8 +63,8 @@ In theory, FTPS should work too, but it is completely untested.
 
 ## Gopher
 
-Gopher is supported through the `adapter/protocol/gopher.nim` libcurl
-adapter. Gopher directories are passed as the `text/gopher` type, and
+Gopher is supported through the `adapter/protocol/gopher.nim` adapter.
+Gopher directories are passed as the `text/gopher` type, and
 `adapter/format/gopher.nim` takes care of converting this to HTML.
 
 Gopher selector types are converted to MIME types when possible; note however,
diff --git a/res/urimethodmap b/res/urimethodmap
index 356e1e4b..91032615 100644
--- a/res/urimethodmap
+++ b/res/urimethodmap
@@ -10,7 +10,6 @@ ftp:			cgi-bin:ftp
 sftp:			cgi-bin:ftp
 ftps:			cgi-bin:ftp
 gopher:			cgi-bin:gopher
-gophers:		cgi-bin:gophers
 spartan:		cgi-bin:spartan
 man:			cgi-bin:man
 man-k:			cgi-bin:man
diff --git a/src/io/dynstream.nim b/src/io/dynstream.nim
index e9c69c58..4a711c12 100644
--- a/src/io/dynstream.nim
+++ b/src/io/dynstream.nim
@@ -80,6 +80,9 @@ proc recvDataLoop*(s: DynStream; buffer: pointer; len: int) =
 proc recvDataLoop*(s: DynStream; buffer: var openArray[uint8]) {.inline.} =
   s.recvDataLoop(addr buffer[0], buffer.len)
 
+proc recvDataLoop*(s: DynStream; buffer: var openArray[char]) {.inline.} =
+  s.recvDataLoop(addr buffer[0], buffer.len)
+
 proc recvAll*(s: DynStream): string =
   var buffer = newString(4096)
   var idx = 0
@@ -161,7 +164,10 @@ method sclose*(s: PosixStream) =
   s.closed = true
 
 proc newPosixStream*(fd: FileHandle): PosixStream =
-  return PosixStream(fd: fd, blocking: true)
+  return PosixStream(fd: cint(fd), blocking: true)
+
+proc newPosixStream*(fd: SocketHandle): PosixStream =
+  return PosixStream(fd: cint(fd), blocking: true)
 
 proc newPosixStream*(path: string; flags, mode: cint): PosixStream =
   let fd = open(cstring(path), flags, mode)
diff --git a/src/loader/connecterror.nim b/src/loader/connecterror.nim
index 878bc8d0..1ee4e48b 100644
--- a/src/loader/connecterror.nim
+++ b/src/loader/connecterror.nim
@@ -25,6 +25,9 @@ type ConnectionError* = enum
   ceProxyRefusedToConnect = (6, "ProxyRefusedToConnect")
   ceFailedToResolveHost = (7, "FailedToResolveHost")
   ceFailedToResolveProxy = (8, "FailedToResolveProxy")
+  ceProxyAuthFail = (9, "ProxyAuthFail")
+  ceInvalidResponse = (10, "InvalidResponse")
+  ceProxyInvalidResponse = (11, "ProxyInvalidResponse")
 
 const ErrorMessages* = [
   ceCGIOutputHandleNotFound: "request body output handle not found",
@@ -53,6 +56,9 @@ const ErrorMessages* = [
   ceProxyRefusedToConnect: "proxy refused to connect",
   ceFailedToResolveHost: "failed to resolve host",
   ceFailedToResolveProxy: "failed to resolve proxy",
+  ceProxyAuthFail: "proxy authentication failed",
+  ceInvalidResponse: "received an invalid response",
+  ceProxyInvalidResponse: "proxy returned an invalid response",
 ]
 
 converter toInt*(code: ConnectionError): int =