@fanoutio/serve-grip
Version:
Connect-style Middleware for GRIP
202 lines (201 loc) • 7.67 kB
JavaScript
import debug from '../debug.js';
import { encodeWebSocketEvents, Channel, isNodeReqWsOverHttp, getWebSocketContextFromNodeReq, } from '@fanoutio/grip';
import { ServeGripBase } from '../ServeGripBase.js';
export class ServeGrip extends ServeGripBase {
koa;
constructor(config, fn = 'exec') {
super(config, fn);
this.koa = async (ctx, next) => {
await this.run(ctx.req, ctx.res);
await next();
};
}
exec(req, res, fn) {
debug('ServeGrip#exec - serveGrip invoked as Middleware function');
let err;
this.run(req, res)
.catch((ex) => (err = ex))
.then((result) => {
if (err !== undefined) {
fn(err);
}
else {
if (result) {
fn();
}
}
});
}
getRequestGrip(req) {
return req.grip;
}
setRequestGrip(req, grip) {
req.grip = grip;
}
isRequestWsOverHttp(req) {
return isNodeReqWsOverHttp(req);
}
getRequestWebSocketContext(req) {
return getWebSocketContextFromNodeReq(req);
}
getRequestHeaderValue(req, key) {
let value = req.headers[key];
if (Array.isArray(value)) {
value = value[0];
}
return value;
}
setResponseGrip(res, grip) {
res.grip = grip;
}
setResponseStatus(res, code) {
res.statusCode = code;
}
endResponse(res, chunk) {
return res.end(chunk);
}
onAfterSetup(params) {
// ## Monkey-patch res methods
const { res, wsContext, gripInstructGetter } = params;
if (wsContext != null) {
debug('Monkey-patch res methods for WS-over-HTTP - start');
this.monkeyPatchResMethodsForWebSocket(res, wsContext);
debug('Monkey-patch res methods for WS-over-HTTP - end');
}
else {
debug('Monkey-patch res methods for GripInstruct - start');
this.monkeyPatchResMethodsForGripInstruct(res, gripInstructGetter);
debug('Monkey-patch res methods for GripInstruct - end');
}
}
monkeyPatchResMethodsForWebSocket(res, wsContext) {
debug('res.removeHeader');
const resRemoveHeader = res.removeHeader;
// @ts-ignore
res.removeHeader = (name) => {
debug('res.removeHeader - start');
// If we have a WsContext, then we don't want to allow removing
// the following headers.
let skip = false;
if (name != null) {
const nameLower = name.toLowerCase();
if (nameLower === 'content-type' ||
nameLower === 'content-length' ||
nameLower === 'transfer-encoding') {
// turn into a no-op
skip = true;
}
}
if (!skip) {
debug('not skipping removeHeader', name);
resRemoveHeader.call(res, name);
}
else {
debug('skipping removeHeader', name);
}
debug('res.removeHeader - end');
};
debug('res.writeHead');
const resWriteHead = res.writeHead;
// @ts-ignore
res.writeHead = (statusCode, reason, obj) => {
debug('res.writeHead - start');
if (typeof reason === 'string') {
// assume this was called like this:
// writeHead(statusCode, reasonPhrase[, headers])
}
else {
// this was called like this:
// writeHead(statusCode[, headers])
obj = reason;
}
debug('res.statusCode', res.statusCode);
if (statusCode === 200 || statusCode === 204) {
const wsContextHeaders = wsContext.toHeaders();
debug("Adding wsContext headers", wsContextHeaders);
obj = Object.assign({}, obj, wsContextHeaders);
// Koa will set status code 204 when the body has been set to
// null. This is probably fine since the main stream
// for WS-over-HTTP is supposed to have an empty
// body anyway. However, we will be adding WebSocket
// events into the body, so change it to a 200.
statusCode = 200;
reason = 'OK';
}
debug('res.writeHead - end');
if (typeof reason === 'string') {
// @ts-ignore
resWriteHead.call(res, statusCode, reason, obj);
}
else {
resWriteHead.call(res, statusCode, obj);
}
};
debug('res.end');
const resEnd = res.end;
// @ts-ignore
res.end = (chunk, encoding, callback) => {
debug('res.end - start');
debug('res.statusCode', res.statusCode);
if (res.statusCode === 200 || res.statusCode === 204) {
debug('Getting outgoing events');
const events = wsContext.getOutgoingEvents();
debug('Encoding and writing events', events);
res.write(encodeWebSocketEvents(events));
}
debug('res.end - end');
// @ts-ignore
resEnd.call(res, chunk, encoding, callback);
};
}
monkeyPatchResMethodsForGripInstruct(res, gripInstructGetter) {
debug('res.writeHead');
const resWriteHead = res.writeHead;
// @ts-ignore
res.writeHead = (statusCode, reason, obj) => {
debug('res.writeHead - start');
if (typeof reason === 'string') {
// assume this was called like this:
// writeHead(statusCode, reasonPhrase[, headers])
}
else {
// this was called like this:
// writeHead(statusCode[, headers])
obj = reason;
}
debug('res.statusCode', res.statusCode);
const gripInstruct = gripInstructGetter();
if (gripInstruct != null) {
debug("GripInstruct present");
if (statusCode === 304) {
// Code 304 only allows certain headers.
// Some web servers strictly enforce this.
// In that case we won't be able to use
// Grip- headers to talk to the proxy.
// Switch to code 200 and use Grip-Status
// to specify intended status.
debug("Using gripInstruct setStatus header to handle 304");
statusCode = 200;
reason = 'OK';
gripInstruct.setStatus(304);
}
// Apply prefix to channel names
gripInstruct.channels = gripInstruct.channels.map((ch) => new Channel(this.prefix + ch.name, ch.prevId));
const gripInstructHeaders = gripInstruct.toHeaders();
debug("Adding GripInstruct headers", gripInstructHeaders);
obj = Object.assign({}, obj, gripInstructHeaders);
}
else {
debug("GripInstruct not present");
}
debug('res.writeHead - end');
if (typeof reason === 'string') {
// @ts-ignore
resWriteHead.call(res, statusCode, reason, obj);
}
else {
resWriteHead.call(res, statusCode, obj);
}
};
}
}