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 = """ """ if op.t == DIRECTORY: heads &= "

Index of " & htmlEscape(op.surl) & "

" else: # search heads &= "

Search " & htmlEscape(op.surl) & "

" 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 = """

Search """ & htmlEscape(op.surl) & """

""" 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 = "
"
    line &= htmlEscape(name) & "\n"
  else:
    if op.ispre:
      line = "
" 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 &= "" & names & "
\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], p, 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("\n") discard op.handle.sendData("\n\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