2018-11-04 20:01:40 +00:00
|
|
|
//---------------------------------------------------------------------------
|
|
|
|
// @syndicate-lang/driver-http-node, HTTP support for Syndicate/js
|
|
|
|
// Copyright (C) 2016-2018 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
//---------------------------------------------------------------------------
|
|
|
|
|
2018-11-15 23:24:58 +00:00
|
|
|
import { genUuid, Seal, Capture, Observe, Dataspace, currentFacet, Bytes } from "@syndicate-lang/core";
|
2018-11-04 20:01:40 +00:00
|
|
|
import { parse as parseUrl } from "url";
|
|
|
|
|
|
|
|
const http = require('http');
|
|
|
|
const https = require('https');
|
|
|
|
const _WebSocket = require('ws');
|
|
|
|
|
|
|
|
assertion type HttpServer(host, port);
|
|
|
|
assertion type HttpsServer(host, port, options);
|
|
|
|
|
|
|
|
assertion type WebSocket(id, server, path, query);
|
|
|
|
assertion type Request(id, server, method, path, query, req);
|
|
|
|
|
|
|
|
assertion type Response(id, code, message, headers, detail);
|
2018-11-05 17:48:50 +00:00
|
|
|
|
|
|
|
message type DataIn(id, chunk);
|
|
|
|
message type DataOut(id, chunk);
|
2018-11-04 20:01:40 +00:00
|
|
|
|
|
|
|
Object.assign(module.exports, {
|
|
|
|
HttpServer, HttpsServer,
|
2018-11-05 17:48:50 +00:00
|
|
|
WebSocket, Request, DataIn,
|
|
|
|
Response, DataOut,
|
2018-11-04 20:01:40 +00:00
|
|
|
});
|
|
|
|
|
2018-11-15 23:24:58 +00:00
|
|
|
spawn named 'driver/HttpServerFactory' {
|
2018-11-04 20:01:40 +00:00
|
|
|
during Observe(Request(_, HttpServer($h, $p), _, _, _, _)) assert HttpServer(h, p);
|
|
|
|
during Observe(Request(_, HttpsServer($h, $p, $o), _, _, _, _)) assert HttpsServer(h, p, o);
|
|
|
|
|
2018-11-15 23:24:58 +00:00
|
|
|
during HttpServer($host, $port) spawn named ['driver/HttpServer', host, port] {
|
2018-11-04 20:01:40 +00:00
|
|
|
_server.call(this, host, port, null);
|
|
|
|
}
|
2018-11-15 23:24:58 +00:00
|
|
|
during HttpsServer($host, $port, $options) spawn named ['driver/HttpsServer', host, port] {
|
2018-11-04 20:01:40 +00:00
|
|
|
_server.call(this, host, port, options);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function _server(host, port, httpsOptions) {
|
|
|
|
const server = httpsOptions ? HttpsServer(host, port, httpsOptions) : HttpServer(host, port);
|
|
|
|
|
|
|
|
const requestHandlerMap = {};
|
|
|
|
const wsHandlerMap = {};
|
|
|
|
|
2018-11-15 11:00:13 +00:00
|
|
|
function isPathPattern(p) {
|
|
|
|
// Loose, but good enough for distinguishing
|
|
|
|
// List-of-constants-and-captures from just a straight capture.
|
|
|
|
// TODO: Still really not the best idea. Reconsider schema.
|
|
|
|
return typeof p === 'object' && p !== null && typeof p.toJS === 'function';
|
|
|
|
}
|
|
|
|
|
2018-11-04 20:01:40 +00:00
|
|
|
function encodePath(path) {
|
2018-11-13 14:08:10 +00:00
|
|
|
return JSON.stringify(path.toJS().map((s) => Capture.isClassOf(s) ? null : s));
|
2018-11-04 20:01:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
during Observe(Request(_, server, $method, $pathPattern, _, _)) {
|
2018-11-15 11:00:13 +00:00
|
|
|
if (typeof method !== 'string' || !isPathPattern(pathPattern)) {
|
2018-11-13 20:53:45 +00:00
|
|
|
// Likely some kind of logging observer.
|
|
|
|
// TODO: reconsider schema
|
|
|
|
return;
|
|
|
|
}
|
2018-11-04 20:01:40 +00:00
|
|
|
if (method.toLowerCase() !== method) {
|
|
|
|
console.warn('HTTP method should be lowercase: ' + method);
|
|
|
|
}
|
|
|
|
const path = encodePath(pathPattern);
|
|
|
|
on start {
|
|
|
|
if (!(path in requestHandlerMap)) requestHandlerMap[path] = {_count: 0, _path: pathPattern};
|
|
|
|
requestHandlerMap[path]._count++;
|
|
|
|
if (!(method in requestHandlerMap[path])) requestHandlerMap[path][method] = 0;
|
|
|
|
requestHandlerMap[path][method]++;
|
|
|
|
}
|
|
|
|
on stop {
|
|
|
|
requestHandlerMap[path][method]--;
|
|
|
|
if (requestHandlerMap[path][method] === 0) delete requestHandlerMap[path][method];
|
|
|
|
requestHandlerMap[path]._count--;
|
|
|
|
if (requestHandlerMap[path]._count === 0) delete requestHandlerMap[path];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-04 23:00:32 +00:00
|
|
|
during Observe(WebSocket(_, server, $pathPattern, _)) {
|
2018-11-15 11:00:13 +00:00
|
|
|
if (!isPathPattern(pathPattern)) {
|
|
|
|
// Likely some kind of logging observer.
|
|
|
|
// TODO: reconsider schema
|
|
|
|
return;
|
|
|
|
}
|
2018-11-04 23:00:32 +00:00
|
|
|
const path = encodePath(pathPattern);
|
2018-11-04 20:01:40 +00:00
|
|
|
on start {
|
2018-11-04 23:00:32 +00:00
|
|
|
if (!(path in wsHandlerMap)) wsHandlerMap[path] = {_count: 0, _path: pathPattern};
|
|
|
|
wsHandlerMap[path]._count++;
|
2018-11-04 20:01:40 +00:00
|
|
|
}
|
|
|
|
on stop {
|
2018-11-04 23:00:32 +00:00
|
|
|
wsHandlerMap[path]._count--;
|
|
|
|
if (wsHandlerMap[path]._count === 0) delete wsHandlerMap[path];
|
2018-11-04 20:01:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const s = httpsOptions ? https.createServer(httpsOptions) : http.createServer();
|
|
|
|
const wss = new _WebSocket.Server({ server: s, verifyClient: checkWSConnection });
|
|
|
|
|
|
|
|
function pathMatches(path, pieces) {
|
|
|
|
if (path.length !== pieces.length) return false;
|
|
|
|
for (let i = 0; i < path.length; i++) {
|
|
|
|
if (path[i] !== null && path[i] !== pieces[i]) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
function mapLookup(m, pieces) {
|
|
|
|
for (let pathEnc in m) {
|
|
|
|
if (pathMatches(JSON.parse(pathEnc), pieces)) {
|
|
|
|
return m[pathEnc];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
function reqUrlPieces(url) {
|
|
|
|
let pieces = url.pathname.split(/\//);
|
|
|
|
while (pieces[0] === '') pieces.shift();
|
|
|
|
while (pieces.length && pieces[pieces.length - 1] === '') pieces.pop();
|
|
|
|
return pieces;
|
|
|
|
}
|
|
|
|
|
|
|
|
s.on('request', Dataspace.wrapExternal((req, res) => {
|
|
|
|
let url = parseUrl(req.url, true);
|
|
|
|
let pieces = reqUrlPieces(url);
|
|
|
|
let methodMap = mapLookup(requestHandlerMap, pieces);
|
|
|
|
if (!methodMap) {
|
|
|
|
res.writeHead(404, "Not found", {});
|
|
|
|
res.end();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let pathPattern = methodMap._path;
|
|
|
|
let method = req.method.toLowerCase();
|
|
|
|
if (!(method in methodMap)) {
|
|
|
|
res.writeHead(405, "Method not allowed", {});
|
|
|
|
res.end();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
react {
|
2018-11-05 11:19:52 +00:00
|
|
|
const facet = currentFacet();
|
2018-11-05 21:16:57 +00:00
|
|
|
let id = genUuid('_httpRequest');
|
2018-11-04 20:01:40 +00:00
|
|
|
assert Request(id, server, method, pieces, url.query, Seal(req));
|
|
|
|
stop on retracted Observe(Request(_, server, method, pathPattern, _, _)) {
|
|
|
|
// Error resulting in teardown of the route
|
|
|
|
res.writeHead(500, "Internal server error", {});
|
|
|
|
res.end();
|
|
|
|
}
|
|
|
|
stop on retracted Observe(Request(id, server, method, pieces, _, _)) {
|
|
|
|
// Error specific to this particular request
|
|
|
|
res.writeHead(500, "Internal server error", {});
|
|
|
|
res.end();
|
|
|
|
}
|
|
|
|
on asserted Response(id, $code, $message, $headers, $detail)
|
|
|
|
{
|
|
|
|
res.writeHead(code, message, headers.toJS());
|
|
|
|
if (detail === null) {
|
|
|
|
react {
|
|
|
|
on retracted Response(id, code, message, headers, detail) {
|
|
|
|
res.end();
|
|
|
|
facet.stop();
|
|
|
|
}
|
2018-11-05 17:48:50 +00:00
|
|
|
on message DataOut(id, $chunk) {
|
2018-11-21 14:22:58 +00:00
|
|
|
res.write(Buffer.from(Bytes.toIO(chunk)));
|
2018-11-04 20:01:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2018-11-21 14:22:58 +00:00
|
|
|
res.end(Buffer.from(Bytes.toIO(detail)));
|
2018-11-04 20:01:40 +00:00
|
|
|
facet.stop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
|
|
|
|
function checkWSConnection(info, callback) {
|
|
|
|
let url = parseUrl(info.req.url, true);
|
|
|
|
let pieces = reqUrlPieces(url);
|
2018-11-04 23:00:32 +00:00
|
|
|
if (!mapLookup(wsHandlerMap, pieces)) {
|
2018-11-04 20:01:40 +00:00
|
|
|
callback(false, 404, "Not found", {});
|
|
|
|
} else {
|
|
|
|
callback(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-04 23:00:32 +00:00
|
|
|
wss.on('connection', Dataspace.wrapExternal((ws, req) => {
|
|
|
|
let url = parseUrl(req.url, true);
|
2018-11-04 20:01:40 +00:00
|
|
|
let pieces = reqUrlPieces(url);
|
2018-11-04 23:00:32 +00:00
|
|
|
let { _path: pathPattern } = mapLookup(wsHandlerMap, pieces);
|
|
|
|
|
|
|
|
react {
|
2018-11-05 11:19:52 +00:00
|
|
|
const facet = currentFacet();
|
2018-11-05 21:16:57 +00:00
|
|
|
let id = genUuid('_wsRequest');
|
2018-11-04 23:00:32 +00:00
|
|
|
assert WebSocket(id, server, pieces, url.query);
|
|
|
|
|
|
|
|
on stop ws.close();
|
|
|
|
|
|
|
|
ws.on('close', Dataspace.wrapExternal(() => {
|
|
|
|
facet.stop();
|
|
|
|
}));
|
|
|
|
|
2018-11-05 17:48:50 +00:00
|
|
|
on asserted Observe(DataIn(id, _)) {
|
2018-11-04 23:00:32 +00:00
|
|
|
ws.on('message', Dataspace.wrapExternal((message) => {
|
2018-11-16 11:17:59 +00:00
|
|
|
send DataIn(id, Bytes.fromIO(message));
|
2018-11-04 23:00:32 +00:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2018-11-05 17:48:50 +00:00
|
|
|
on message DataOut(id, $message) {
|
2018-11-15 23:24:58 +00:00
|
|
|
ws.send(Bytes.toIO(message));
|
2018-11-04 23:00:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
stop on retracted Observe(WebSocket(_, server, pathPattern, _));
|
|
|
|
stop on retracted Observe(WebSocket(id, server, pieces, _));
|
|
|
|
}
|
|
|
|
}));
|
2018-11-04 20:01:40 +00:00
|
|
|
|
|
|
|
on start s.listen(port, host);
|
2018-11-04 23:00:32 +00:00
|
|
|
on stop {
|
|
|
|
wss.close();
|
|
|
|
s.close();
|
|
|
|
}
|
2018-11-04 20:01:40 +00:00
|
|
|
}
|