about summary refs log tree commit diff stats
path: root/adapter/protocol
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 /adapter/protocol
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.
Diffstat (limited to 'adapter/protocol')
-rw-r--r--adapter/protocol/gopher.nim127
-rw-r--r--adapter/protocol/lcgi.nim149
2 files changed, 202 insertions, 74 deletions
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")