little-api
Version:
A simple JSON-over-HTTP/WS RPC server and client
140 lines (124 loc) • 3.43 kB
JavaScript
var url = require("url");
var http = require("http");
var express = require("express");
var cors = require("cors");
var bodyParser = require("body-parser");
var WebSocket = require("ws");
var debug = require("debug")("little-api");
function handleError(err, res) {
res.status(500).send({
success: false,
err: {
name: err.name,
message: err.message,
stack: err.stack,
code: err.code,
},
});
}
function handleSuccess(result, res) {
res.status(200).send({
success: true,
result: result,
});
}
function b64Decode(b64String) {
var buf;
if (typeof Buffer.from === "function") {
buf = Buffer.from(b64String, "base64");
} else {
buf = new Buffer(b64String, "base64");
}
return buf.toString("utf-8");
}
function decodeSocketData(request) {
var url = request.url.slice(1); // remove leading '/'
var data = JSON.parse(b64Decode(url));
return data;
}
module.exports = function createServer(config) {
debug("createServer", config);
if (typeof config !== "object" || config == null) {
throw new Error(
"Invalid config provided to createServer. You must provide an object."
);
}
var app = express();
app.use(
bodyParser.json({
limit: config.requestSizeLimit || "1GB",
})
);
if (!config.noCors) {
app.use(cors(config.corsOptions));
app.options("*", cors());
}
app.post("/", function handleRequest(req, res) {
debug("handleRequest", req.body);
try {
var methodName = req.body.methodName;
if (methodName == null) {
throw new Error("No method name specified");
}
var method = config.methods[methodName];
if (method == null) {
throw new Error("No such method: '" + methodName + "'");
}
var args = req.body.args;
var result = method.apply(null, args);
if (
typeof result === "object" &&
result != null &&
typeof result.then === "function"
) {
result.then(
function onResolve(value) {
handleSuccess(value, res);
},
function onReject(err) {
handleError(err, res);
}
);
} else {
handleSuccess(result, res);
}
} catch (err) {
handleError(err, res);
}
});
if (config.withApp) {
config.withApp(app);
}
var server = http.createServer(app);
var wsServer = new WebSocket.Server({ server: server });
wsServer.on("connection", function onConnection(socket, request) {
try {
var data = decodeSocketData(request);
} catch (err) {
socket.close(4400, "Invalid request");
return;
}
var methodName = data.methodName;
var args = data.args;
try {
var method = config.socketMethods[methodName];
if (method == null) {
socket.close(4404, "No such socket method: '" + methodName + "'");
return;
} else {
method.apply(null, [socket].concat(args));
}
} catch (err) {
// According to RFC6455, a WebSocket control frame must be 125 bytes or
// smaller, and in a Close control frame, the first two bytes are the
// status code (in this case, 4500). That leaves 123 bytes left for the
// reason string.
var maxReasonSize = 123;
socket.close(
4500,
err.stack.replace(/\n {4}at/g, "\nat").slice(0, maxReasonSize)
);
}
});
return server;
};