# 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 nativesockets
import options
import streams
import tables
import net
when defined(posix):
import posix
import bindings/curl
import io/about
import io/file
import io/http
import io/request
import io/urlfilter
import ips/serialize
import ips/serversocket
import ips/socketstream
import types/cookie
import types/mime
import types/referer
import types/url
import utils/twtstr
type
FileLoader* = object
process*: Pid
LoaderCommand = enum
LOAD, QUIT
LoaderConfig* = object
defaultheaders*: HeaderList
filter*: URLFilter
cookiejar*: CookieJar
referrerpolicy*: ReferrerPolicy
proc loadResource(request: Request, ostream: Stream) =
case request.url.scheme
of "file":
loadFile(request.url, ostream)
of "http", "https":
loadHttp(request, ostream)
of "about":
loadAbout(request, ostream)
else:
ostream.swrite(-1) # error
ostream.flush()
var ssock: ServerSocket
proc runFileLoader*(fd: cint, config: LoaderConfig) =
if curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK:
raise newException(Defect, "Failed to initialize libcurl.")
ssock = initServerSocket()
# 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)
onSignal SIGTERM, SIGINT:
curl_global_cleanup()
ssock.close()
quit(1)
while true:
let stream = ssock.acceptSocketStream()
try:
var cmd: LoaderCommand
stream.sread(cmd)
case cmd
of LOAD:
var request: Request
stream.sread(request)
if not config.filter.match(request.url):
stream.swrite(-1) # error
stream.flush()
else:
for k, v in config.defaultHeaders.table:
if k notin request.headers.table:
request.headers.table[k] = v
if config.cookiejar != nil and config.cookiejar.cookies.len > 0:
if "Cookie" notin request.headers.table:
let cookie = config.cookiejar.serialize(request.url)
if cookie != "":
request.headers["Cookie"] = cookie
if request.referer != nil and "Referer" notin request.headers.table:
let r = getReferer(request.referer, request.url, config.referrerpolicy)
if r != "":
request.headers["Referer"] = r
loadResource(request, stream)
stream.close()
of QUIT:
stream.close()
break
except EOFError:
# End-of-file, quit.
break
stream.close()
curl_global_cleanup()
ssock.close()
quit(0)
#TODO async requests...
proc doRequest*(loader: FileLoader, request: Request, blocking = true): Response =
new(result)
let stream = connectSocketStream(loader.process, false, blocking = true)
stream.swrite(LOAD)
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:
if result.status in 301..303 or result.status in 307..308:
let location = result.headers.table["Location"][0]
let url = parseUrl(location, some(request.url))
if url.isSome:
if (result.status == 303 and
request.httpmethod notin {HTTP_GET, HTTP_HEAD}) or
(result.status == 301 or result.status == 302 and
request.httpmethod == HTTP_POST):
result.redirect = newRequest(url.get, HTTP_GET,
mode = request.mode, credentialsMode = request.credentialsMode,
destination = request.destination)
else:
result.redirect = newRequest(url.get, request.httpmethod,
body = request.body, multipart = request.multipart,
mode = request.mode, credentialsMode = request.credentialsMode,
destination = request.destination)
# Only a stream of the response body may arrive after this point.
result.body = stream
if not blocking:
stream.source.getFd().setBlocking(blocking)
proc quit*(loader: FileLoader) =
let stream = connectSocketStream(loader.process)
if stream != nil:
stream.swrite(QUIT)