@needle-tools/networking
Version:
Networking backend functionality for Needle Engine
136 lines (115 loc) • 4.13 kB
JavaScript
/**
* @param {import("express").Express} app
* @param {import("../@types/index").NetworkingOptions} options
*/
module.exports.startServerExpress = function (app, options) {
const networking = create(app, options);
// necessary to make app.ws work
require("express-ws")(app, options.server);
const endpoint = getEndpoint(options);
console.log("Using endpoint", endpoint);
app.ws(endpoint, function (ws, request) {
networking.onConnection(ws);
});
setupBlob(app);
return networking;
};
const { FastifyProxy } = require("./proxy");
const { blobStorage } = require("./storage");
/**
* @param {import("fastify").FastifyInstance} fastify
* @param {import("../@types/index").NetworkingOptions} options
*/
module.exports.startServerFastify = function (fastify, options) {
const networking = create(fastify, options);
fastify.register(require('fastify-websocket'));
const endpoint = getEndpoint(options)
console.log("Using endpoint", endpoint);
fastify.get(endpoint, { websocket: true }, (connection, req) => {
const proxy = new FastifyProxy(connection);
networking.onConnection(proxy);
});
setupBlob(fastify);
return networking;
};
function getEndpoint(options) {
if (options && options.endpoint !== undefined && typeof options.endpoint === "string") {
return options.endpoint;
}
return "/";
}
function create(app, options) {
console.info("Starting networking server");
const networking = require("./networking");
networking.init(app, options);
return networking;
}
function setupBlob(app) {
app.get("/api/needle/blob/:key", async (request, response) => {
const key = request.params.key;
console.log("Requesting blob", key);
const url = await blobStorage.getDownloadURL(key);
// if we get a string it's the download url
if (typeof url === "string") {
return response.status(302).redirect(url);
}
response.status(400).send(url);
});
app.post("/api/needle/blob", async (request, response) => {
// prevent users from uploading too many files
const origin = request.headers["origin"];
const userAgent = request.headers["user-agent"];
if (!allowUpload(origin, userAgent)) {
console.log("Upload request blocked", origin, userAgent);
return response.status(429).send({ error: "too many requests" });
}
// prevent files from being too large
const maxSize = 50_000_000;
if (parseInt(request.headers["filesize"]) > maxSize) {
console.log("Upload request too large", request.headers["filesize"]);
return response.status(400).send({ error: "file too large" });
}
const data = {
filename: request.headers["filename"],
content_md5: request.headers["content-md5"],
key: request.headers["content-md5"],
content_type: request.headers["content-type"],
content_length: parseInt(request.headers["filesize"]),
}
// the key is base64 encoded, make sure we don't have any slashes
// hence we need to replace them with underscores
data.key = data.key.replace(/\//g, "_");
const obj = await blobStorage.getUploadURL(data);
if (!obj) {
return response.status(400).send({ error: "unknown error" });
}
// if we get an error object, return it
if ("error" in obj) {
return response.status(400).send(obj);
}
if (!("key" in obj)) {
return response.status(400).send({ error: "no key" });
}
const result = {
...obj,
key: data.key,
download: `/api/needle/blob/${data.key}`,
}
response.send(result);
});
const requestCount = new Map();
function allowUpload(origin, userAgent) {
const key = origin + userAgent;
let count = requestCount.get(key);
if (count === undefined) {
count = 0;
// reset count after 1 minute
setTimeout(() => { requestCount.delete(key); }, 60_000);
}
if (count >= 30) {
return false;
}
requestCount.set(key, count + 1);
return true;
}
}