import os import io import errno import json let FCGI_LISTENSOCK_FILENO = 0; let FCGI_HEADER_LEN = 8; let FCGI_VERSION_1 = 1; let FCGI_BEGIN_REQUEST = 1; let FCGI_ABORT_REQUEST = 2; let FCGI_END_REQUEST = 3; let FCGI_PARAMS = 4; let FCGI_STDIN = 5; let FCGI_STDOUT = 6; let FCGI_STDERR = 7; let FCGI_DATA = 8; let FCGI_GET_VALUES = 9; let FCGI_GET_VALUES_RESULT = 10; let FCGI_UNKNOWN_TYPE = 11; let FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE; let FCGI_NULL_REQUEST_ID = 0; let FCGI_KEEP_CONN = 1; let FCGI_RESPONDER = 1; let FCGI_AUTHORIZER = 2; let FCGI_FILTER = 3; let FCGI_REQUEST_COMPLETE = 0; let FCGI_CANT_MPX_CONN = 1; let FCGI_OVERLOADED = 2; let FCGI_UNKNOWN_ROLE = 3; let FCGI_MAX_CONNS = "FCGI_MAX_CONNS"; let FCGI_MAX_REQS = "FCGI_MAX_REQS"; let FCGI_MPXS_CONNS = "FCGI_MPXS_CONNS"; let statusText = %{ 200: 'OK', 400: 'Bad Request', 401: 'Unauthorized', 404: 'Not Found' } class FCGIParser { init(fd) { @fd = fd @buffer = Blob() @parser = self.start() (@parser)() } next(n) { while #@buffer < (n ?? 1) { @buffer.push(yield nil) } return if n == nil { @buffer.splice(0, 1)[0] } else { @buffer.splice(0, n) } } start*() { while true { let body = blob() let params = %{} while true { let next = self.next let nextShort = function () { let b = next(2) (b[0] << 8) + b[1] } let r = FCGIRecord(); r.version = next() r.type = next() r.requestID = nextShort() let contentLength = nextShort() let paddingLength = next() next() r.content = next(contentLength) next(paddingLength) match r.type { ::FCGI_BEGIN_REQUEST => { r.beginRequest() }, ::FCGI_PARAMS => { params += r.getParams() }, ::FCGI_STDIN => { if #r.content == 0 { break } else { body.push(r.content) } }, t => { print("Type = {t}") } } } yield FCGIRequest(nil, body.str!(), params, @fd) } } push(data) { if let Some($request) = (@parser)(data) { request } } } class FCGIRecord { beginRequest() { } getParams() { let b = @content let ps = %{} while b.size() != 0 { let nameLength = getLength(b) let valueLength = getLength(b) let name = b.splice(0, nameLength).str() let value = b.splice(0, valueLength).str() ps[name] = value } return @params = ps } } function getLength(b) { if (b[0] >> 7) == 1 { let n = ((b[0] & 0x7F) << 24) + (b[1] << 16) + (b[2] << 8) + b[3] b.splice(0, 4) return n } else { let n = b[0] b.splice(0, 1) return n } } function decode(s) { s.sub('+', ' ') .sub(/%../, s -> chr(int(s.slice(1), 16))) .sub("\r\n", "\n") .sub("\r", "\n") } function urlDecode(s) { let components = s.split('&'); let pairs = [c.split('=').map(decode) for c in components]; return %{k: v for [k, v] in pairs}; } class FCGIRequest { init(stream, body, ps, fd) { @stream = stream @fd = fd self.body = body self.params = ps } sendHTML(html, headers=[], status=200) { self.sendResponse(status, html, [('Content-Type', 'text/html'), *headers]) } sendJSON(x, headers=[], status=200) { self.sendResponse(status, json.encode(x), [('Content-Type', 'application/json'), *headers]) } sendResponse(status, body='', headers=[]) { let buffer = blob() let push = buffer.push let exitStatus = 0 let output = blob() for (k, v) in headers { output.push("{k}: {v}\r\n") } output.push("Status: {status} {statusText[status]}\r\n\r\n") output.push(body) let size = #output let offset = 0 while size > 0xFFFF { push(FCGI_VERSION_1); push(FCGI_STDOUT); push(0); push(1); push(0xFF); push(0xFF); push(0); push(0); push(output.slice(offset, 0xFFFF)); @stream.write(buffer) buffer.clear(); size -= 0xFFFF offset += 0xFFFF } if size > 0 { push(FCGI_VERSION_1); push(FCGI_STDOUT); push(0); push(1); push(size >> 8); push(0xFF & size); push(0); push(0); push(output.slice(offset)); @stream.write(buffer) buffer.clear(); } push(FCGI_VERSION_1); push(FCGI_STDOUT); push(0); push(1); push(0); push(0); push(0); push(0); @stream.write(buffer) buffer.clear(); push(FCGI_VERSION_1); push(FCGI_END_REQUEST); push(0); push(1); push(0); push(8); push(0); push(0); push((exitStatus >> 24) & 0xFF); push((exitStatus >> 16) & 0xFF); push((exitStatus >> 8) & 0xFF); push(exitStatus & 0xFF); push(FCGI_REQUEST_COMPLETE); push(0); push(0); push(0); @stream.write(buffer) @stream.flush() @app.death.push(@stream) } queryParams() { return urlDecode(self.params['QUERY_STRING']); } formData() { if self.params['CONTENT_TYPE'].match?(/^multipart\/form-data/) { let form = %{} let [_, delim] = self.body.match!(/^(.*)\r\n/) let offset = 0 while let $i = self.body.bsearch(delim, offset) { if not let $j = self.body.bsearch('\r\n\r\n', i) { break } let headers = self.body.bsplice(i + #delim + 2, j).split(/\r\n/) offset = self.body.bsearch(delim, j) let content = self.body.bsplice(j + 4, offset - 2) let headers = %{k.lower(): v for [k, v] in headers.map(&split(': ', 1))} let disposition = %{k: v for [_, k, v] in headers['content-disposition'].matches(/(\w+)="([^"]+)"/)} let name = disposition['name'] if let $filename = disposition['filename'] { let f = { name: filename, type: headers['content-type'], content: Blob(content) } if let $fs = form[name] { fs.push(f) } else { form[name] = [f] } } else { form[disposition['name']] = content } } return form } else if self.params['CONTENT_TYPE'].match?(/^application\/x-www-form-urlencoded/) { return urlDecode(self.body) } else { return urlDecode(self.params['QUERY_STRING']) } } } pub class FCGIApp { init(socket) { self.socket = socket @death = Sync([]) @clients = %{} } run*() { while true { while #@death > 0 { @death.pop().close() } let pollFds = [ (fd: @socket, events: os.POLLIN) ] for client in @clients { pollFds.push((fd: client, events: os.POLLIN)) } let (t, n) = timed(os.poll(pollFds, 50)) if n == 0 { continue } if pollFds[0].revents & os.POLLIN { let (conn, addr) = os.accept(@socket) @clients[conn] = FCGIParser(conn) } for {fd, events, revents} in pollFds.drop(1) { if revents & os.POLLIN { if let $data = os.read(fd, 4096) { if let $request = @clients[fd].push(data) { request.app = self request.stream = io.open(fd, 'w') yield request } } else { @clients.remove(fd) } } else if revents & (os.POLLNVAL | os.POLLERR) { @clients.remove(fd) } } } } }