diff options
author | bptato <nincsnevem662@gmail.com> | 2023-09-20 01:12:46 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2023-09-20 01:14:15 +0200 |
commit | ecfdd90f7947b65db1046cb2eeeaa8f1953b119f (patch) | |
tree | be0cc64cc1a3138c8d83205e1284309406ce96aa /src/loader | |
parent | a0f3631cc7373b513fa1ae2e9523b1db9dfc6eee (diff) | |
download | chawan-ecfdd90f7947b65db1046cb2eeeaa8f1953b119f.tar.gz |
loader: add gopher support
works
Diffstat (limited to 'src/loader')
-rw-r--r-- | src/loader/gopher.nim | 254 | ||||
-rw-r--r-- | src/loader/headers.nim | 10 | ||||
-rw-r--r-- | src/loader/loader.nim | 5 |
3 files changed, 269 insertions, 0 deletions
diff --git a/src/loader/gopher.nim b/src/loader/gopher.nim new file mode 100644 index 00000000..3ccfc72a --- /dev/null +++ b/src/loader/gopher.nim @@ -0,0 +1,254 @@ +import options +import strutils + +import bindings/curl +import loader/connecterror +import loader/curlhandle +import loader/curlwrap +import loader/headers +import loader/loaderhandle +import loader/request +import types/opt +import types/url +import utils/twtstr + +type GopherType = enum + UNKNOWN = "unsupported" + TEXT_FILE = "text file" + ERROR = "error" + DIRECTORY = "directory" + DOS_BINARY = "DOS binary" + SEARCH = "search" + MESSAGE = "message" + SOUND = "sound" + GIF = "gif" + HTML = "HTML" + INFO = "" + IMAGE = "image" + BINARY = "binary" + PNG = "png" + +type GopherHandle = ref object of CurlHandle + t: GopherType + buffer: string + ispre: bool + surl: string + +func gopherType(c: char): GopherType = + return case c + of '0': TEXT_FILE + of '1': DIRECTORY + of '3': ERROR + of '5': DOS_BINARY + of '7': SEARCH + of 'm': MESSAGE + of 's': SOUND + of 'g': GIF + of 'h': HTML + of 'i': INFO + of 'I': IMAGE + of '9': BINARY + of 'p': PNG + else: UNKNOWN + +func newGopherHandle(curl: CURL, request: Request, handle: LoaderHandle, + t: GopherType): GopherHandle = + return GopherHandle( + curl: curl, + handle: handle, + request: request, + t: t + ) + +proc onStatusLine(op: GopherHandle): bool = + if not op.handle.sendResult(int(CURLE_OK)): + return false + var status: clong + op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status) + if not op.handle.sendStatus(cast[int](status)): + return false + let headers = case op.t + of DIRECTORY, SEARCH, HTML: + newHeaders({"Content-Type": "text/html"}) + of GIF: + newHeaders({"Content-Type": "image/gif"}) + of PNG: + newHeaders({"Content-Type": "image/png"}) + of TEXT_FILE, ERROR: + newHeaders({"Content-Type": "text/plain"}) + else: + newHeaders() + if not op.handle.sendHeaders(headers): + return false + if op.t in {DIRECTORY, SEARCH}: + var heads = """ +<!DOCTYPE HTML> +<HTML> +<HEAD> +<BASE HREF="""" & $op.request.url & """"> +</HEAD> +<BODY> + """ + if op.t == DIRECTORY: + heads &= "<H1>Index of " & htmlEscape(op.surl) & "</H1>" + else: # search + heads &= "<H1>Search " & htmlEscape(op.surl) & "</H1>" + if not op.handle.sendData(heads): + return false + return true + +proc loadSearch(op: GopherHandle) = + discard op.handle.sendResult(int(CURLE_OK)) + discard op.handle.sendStatus(200) # ok + discard op.handle.sendHeaders(newHeaders({"Content-Type": "text/html"})) + var heads = """ +<!DOCTYPE HTML> +<HTML> +<HEAD> +<BASE HREF="""" & $op.request.url & """"> +</HEAD> +<BODY> +<H1>Search """ & htmlEscape(op.surl) & """</H1> +<FORM> +<INPUT TYPE=SEARCH NAME="NAME"> +</FORM> +</BODY> +</HTML> +""" + discard op.handle.sendData(heads) + +proc flushLine(op: GopherHandle, s: string, fromi, toi: int): bool = + if toi == fromi + 1 and s[fromi] == '.': + return true #TODO this is the file end. maybe return false? + if s.len == 0: + return true # invalid + var i = fromi + let tc = s[i] + let t = gopherType(tc) + inc i + let ni = i + while i < toi and s[i] != '\t': inc i + let name = s.substr(ni, i - 1) + inc i + let fi = i + while i < toi and s[i] != '\t': inc i + let file = s.substr(fi, i - 1) + inc i + let hi = i + while i < toi and s[i] != '\t': inc i + let host = s.substr(hi, i - 1) + inc i + let pi = i + while i < toi and s[i] notin {'\t', '\r', '\n'}: inc i + let port = s.substr(pi, i - 1) + var line: string + if t == INFO: + if not op.ispre: + op.ispre = true + line = "<PRE>" + line &= htmlEscape(name) & "\n" + else: + if op.ispre: + line = "</PRE>" + op.ispre = false + let ts = $t + var names = "" + if ts != "": + names &= '[' & ts & ']' + names &= htmlEscape(name) + var ourls: string + if not file.startsWith("URL:"): + let file = if file.len > 0 and file[0] == '/': + file + else: + '/' & file + let pefile = percentEncode(file, PathPercentEncodeSet) + let iurls = "gopher://" & host & ":" & port & "/" & tc & pefile + let url = newURL(iurls) + ourls = if url.isSome: $url.get else: "" + else: + ourls = file.substr("URL:".len) + line &= "<A HREF=\"" & htmlEscape(ourls) & "\">" & names & "</A><BR>\n" + return op.handle.sendData(line) + +proc onSendChunk(op: GopherHandle, previ: int): bool = + var i = previ + var lasti = 0 + while i < op.buffer.len: + if op.buffer[i] in {'\r', '\n'}: + if not op.flushLine(op.buffer, lasti, i): + return false + while i < op.buffer.high and op.buffer[i] in {'\r', '\n'}: + inc i + lasti = i + inc i + if lasti > 0: + op.buffer.delete(0 .. lasti) + return true + +# From the documentation: size is always 1. +proc curlWriteBody(p: cstring, size: csize_t, nmemb: csize_t, + userdata: pointer): csize_t {.cdecl.} = + let op = cast[GopherHandle](userdata) + if not op.statusline: + op.statusline = true + if not op.onStatusLine(): + return 0 + if nmemb > 0: + if op.t in {DIRECTORY, SEARCH}: + let i = op.buffer.len + op.buffer.setLen(op.buffer.len + int(nmemb)) + prepareMutation(op.buffer) + copyMem(addr op.buffer[i], addr p[0], nmemb) + if not op.onSendChunk(i): + return 0 + else: + if not op.handle.sendData(p, int(nmemb)): + return 0 + return nmemb + +proc finish(op: CurlHandle) = + let op = cast[GopherHandle](op) + if op.ispre: + discard op.handle.sendData("</PRE>\n") + discard op.handle.sendData("</BODY>\n</HTML>\n") + +proc loadGopher*(handle: LoaderHandle, curlm: CURLM, + request: Request): CurlHandle = + let curl = curl_easy_init() + doAssert curl != nil + if request.httpmethod != HTTP_GET: + discard handle.sendResult(int(ERROR_INVALID_METHOD)) + return nil + var url = request.url + var path = url.pathname + if path.len < 1: + path &= '/' + if path.len < 2: + path &= '1' + url = newURL(url) + url.setPathname(path) + let t = gopherType(path[1]) + let op = curl.newGopherHandle(request, handle, t) + if t == SEARCH: + if url.query.isNone: + op.surl = url.serialize() + op.loadSearch() + return nil + else: + url.query = some(url.query.get.after('=')) + let surl = url.serialize() + if t in {DIRECTORY, SEARCH}: + op.surl = surl + op.finish = finish + curl.setopt(CURLOPT_URL, surl) + curl.setopt(CURLOPT_WRITEDATA, op) + curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody) + if request.proxy != nil: + let purl = request.proxy.serialize() + curl.setopt(CURLOPT_PROXY, purl) + let res = curl_multi_add_handle(curlm, curl) + if res != CURLM_OK: + discard handle.sendResult(int(res)) + return nil + return op diff --git a/src/loader/headers.nim b/src/loader/headers.nim index b02f30df..e8ef5084 100644 --- a/src/loader/headers.nim +++ b/src/loader/headers.nim @@ -64,6 +64,16 @@ func newHeaders(obj = none(HeadersInit)): Headers {.jsctor.} = headers.fill(obj.get) return headers +func newHeaders*(table: openArray[(string, string)]): Headers = + let headers = Headers() + for (k, v) in table: + let k = k.toHeaderCase() + if k in headers.table: + headers.table[k].add(v) + else: + headers.table[k] = @[v] + return headers + func newHeaders*(table: Table[string, string]): Headers = let headers = Headers() for k, v in table: diff --git a/src/loader/loader.nim b/src/loader/loader.nim index 7ccd9a7f..663915fa 100644 --- a/src/loader/loader.nim +++ b/src/loader/loader.nim @@ -34,6 +34,7 @@ import loader/curlhandle import loader/data import loader/file import loader/ftp +import loader/gopher import loader/headers import loader/http import loader/loaderhandle @@ -119,6 +120,10 @@ proc loadResource(ctx: LoaderContext, request: Request, handle: LoaderHandle) = let handleData = handle.loadFtp(ctx.curlm, request) if handleData != nil: ctx.handleList.add(handleData) + of "gopher", "gophers": + let handleData = handle.loadGopher(ctx.curlm, request) + if handleData != nil: + ctx.handleList.add(handleData) else: discard handle.sendResult(ERROR_UNKNOWN_SCHEME) handle.close() |