diff options
author | bptato <nincsnevem662@gmail.com> | 2024-09-28 17:56:45 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-09-28 17:56:45 +0200 |
commit | 1dea3e9fbe4a902db6325195df0d7a465f82cfc5 (patch) | |
tree | d400bcaa2fdf4c71a81919a45c0a58a345bbc8fc | |
parent | 6a0e957e1f2c9f5bea0882efbf2e0494cd5074fa (diff) | |
download | chawan-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-- | Makefile | 5 | ||||
-rw-r--r-- | adapter/protocol/gopher.nim | 127 | ||||
-rw-r--r-- | adapter/protocol/lcgi.nim | 149 | ||||
-rw-r--r-- | doc/protocols.md | 4 | ||||
-rw-r--r-- | res/urimethodmap | 1 | ||||
-rw-r--r-- | src/io/dynstream.nim | 8 | ||||
-rw-r--r-- | src/loader/connecterror.nim | 6 |
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 = |