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 /adapter/protocol | |
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.
Diffstat (limited to 'adapter/protocol')
-rw-r--r-- | adapter/protocol/gopher.nim | 127 | ||||
-rw-r--r-- | adapter/protocol/lcgi.nim | 149 |
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") |