# A file loader server (?) # The idea here is that we receive requests with a socket, then respond to each # with a response (ideally a document.) # For now, the protocol looks like: # C: Request # S: res (0 => success, _ => error) # if success: # S: status code # S: headers # S: response body # # The body is passed to the stream as-is, so effectively nothing can follow it. import options import streams import tables import net when defined(posix): import posix import bindings/curl import io/http import io/process import io/request import io/serialize import io/socketstream import types/mime import types/url import utils/twtstr const DefaultHeaders0 = { "User-Agent": "chawan", "Accept": "text/html,text/*;q=0.5", "Accept-Language": "en;q=1.0", "Pragma": "no-cache", "Cache-Control": "no-cache", }.toTable() let DefaultHeaders = DefaultHeaders0.newHeaderList() type FileLoader* = ref object defaultHeaders*: HeaderList process*: Pid proc loadFile(url: Url, ostream: Stream) = when defined(windows) or defined(OS2) or defined(DOS): let path = url.path.serialize_unicode_dos() else: let path = url.path.serialize_unicode() let istream = newFileStream(path, fmRead) if istream == nil: ostream.swrite(-1) # error ostream.flush() else: ostream.swrite(0) ostream.swrite(200) # ok ostream.swrite(newHeaderList()) while not istream.atEnd: const bufferSize = 4096 var buffer {.noinit.}: array[bufferSize, char] while true: let n = readData(istream, addr buffer[0], bufferSize) if n == 0: break ostream.writeData(addr buffer[0], n) ostream.flush() if n < bufferSize: break proc loadResource(loader: FileLoader, request: Request, ostream: Stream) = case request.url.scheme of "file": loadFile(request.url, ostream) of "http", "https": loadHttp(request, ostream) else: ostream.swrite(-1) # error ostream.flush() proc runFileLoader(loader: FileLoader, fd: cint) = if curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK: raise newException(Defect, "Failed to initialize libcurl.") let ssock = initServerSocket(getpid()) # The server has been initialized, so the main process can resume execution. var writef: File if not open(writef, FileHandle(fd), fmWrite): raise newException(Defect, "Failed to open input handle.") writef.write(char(0u8)) writef.flushFile() close(writef) discard close(fd) while true: let stream = ssock.acceptSocketStream() try: let request = stream.readRequest() for k, v in loader.defaultHeaders.table: if k notin request.headers.table: request.headers.table[k] = v loader.loadResource(request, stream) except IOError: # End-of-file, quit. # TODO this should be EOFError break stream.close() curl_global_cleanup() ssock.close() quit(0) proc doRequest*(loader: FileLoader, request: Request): Response = new(result) let stream = connectSocketStream(loader.process) stream.swrite(request) stream.flush() stream.sread(result.res) if result.res == 0: stream.sread(result.status) stream.sread(result.headers) if "Content-Type" in result.headers.table: result.contenttype = result.headers.table["Content-Type"][0].until(';') else: result.contenttype = guessContentType($request.url.path) if "Location" in result.headers.table: let location = result.headers.table["Location"][0] result.redirect = parseUrl(location, some(request.url)) # Only a stream of the response body may arrive after this point. result.body = stream proc newFileLoader*(defaultHeaders: HeaderList): FileLoader = new(result) result.defaultHeaders = defaultHeaders when defined(posix): var pipefd: array[0..1, cint] if pipe(pipefd) == -1: raise newException(Defect, "Failed to open pipe.") let pid = fork() if pid == -1: raise newException(Defect, "Failed to fork network process") elif pid == 0: # child process discard close(pipefd[0]) # close read result.runFileLoader(pipefd[1]) else: result.process = pid let readfd = pipefd[0] # get read discard close(pipefd[1]) # close write var readf: File if not open(readf, FileHandle(readfd), fmRead): raise newException(Defect, "Failed to open output handle.") assert readf.readChar() == char(0u8) close(readf) discard close(pipefd[0]) proc newFileLoader*(): FileLoader = newFileLoader(DefaultHeaders)