This commit is contained in:
David Négrier 2020-09-29 10:57:14 +02:00
parent 2cea0e490b
commit a9b1313d39
18 changed files with 32 additions and 1075 deletions

View file

@ -1,10 +1,6 @@
// lib/app.ts
import {IoSocketController} from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..."
import {AuthenticateController} from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..."
import express from "express";
import {Application, Request, Response} from 'express';
import bodyParser = require('body-parser');
import * as http from "http";
import {MapController} from "./Controller/MapController";
import {PrometheusController} from "./Controller/PrometheusController";
import {AdminController} from "./Controller/AdminController";

View file

@ -1,24 +1,26 @@
import {Application, Request, Response} from "express";
import {OK} from "http-status-codes";
import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable";
import Axios from "axios";
import {HttpRequest, HttpResponse} from "uWebSockets.js";
import {parse} from "query-string";
import {App} from "../Server/sifrr.server";
export class AdminController {
App : Application;
constructor(App : Application) {
this.App = App;
constructor(private App : App) {
this.getLoginUrlByToken();
}
getLoginUrlByToken(){
this.App.get("/register/:token", async (req: Request, res: Response) => {
this.App.get("/register/:token", async (res: HttpResponse, req: HttpRequest) => {
if (!ADMIN_API_URL) {
return res.status(500).send('No admin backoffice set!');
return res.writeStatus("500 Internal Server Error").end('No admin backoffice set!');
}
const token:string = req.params.token;
const query = parse(req.getQuery());
const token:string = query.token as string;
let response = null
try {
response = await Axios.get(ADMIN_API_URL+'/api/login-url/'+token, { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} })
@ -30,7 +32,7 @@ export class AdminController {
const organizationSlug = response.data.organizationSlug;
const worldSlug = response.data.worldSlug;
const roomSlug = response.data.roomSlug;
return res.status(OK).send({organizationSlug, worldSlug, roomSlug});
return res.writeStatus("200 OK").end(JSON.stringify({organizationSlug, worldSlug, roomSlug}));
});
}
}

View file

@ -1,6 +1,4 @@
import {Application, Request, Response} from "express";
import Jwt from "jsonwebtoken";
import {BAD_REQUEST, OK} from "http-status-codes";
import {SECRET_KEY, URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
import { uuid } from 'uuidv4';
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js";

View file

@ -1,22 +1,25 @@
import {Application, Request, Response} from "express";
import {OK} from "http-status-codes";
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
import {IoSocketController} from "_Controller/IoSocketController";
import {stringify} from "circular-json";
import {HttpRequest, HttpResponse} from "uWebSockets.js";
import { parse } from 'query-string';
import {App} from "../Server/sifrr.server";
export class DebugController {
constructor(private App : Application, private ioSocketController: IoSocketController) {
constructor(private App : App, private ioSocketController: IoSocketController) {
this.getDump();
}
getDump(){
this.App.get("/dump", (req: Request, res: Response) => {
if (req.query.token !== ADMIN_API_TOKEN) {
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
const query = parse(req.getQuery());
if (query.token !== ADMIN_API_TOKEN) {
return res.status(401).send('Invalid token sent!');
}
return res.status(OK).contentType('application/json').send(stringify(
return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify(
this.ioSocketController.getWorlds(),
(key: unknown, value: unknown) => {
if(value instanceof Map) {

View file

@ -121,7 +121,7 @@ export class IoSocketController {
*
* @param token
*/
searchClientByToken(token: string): ExSocketInterface | null {
/* searchClientByToken(token: string): ExSocketInterface | null {
const clients: ExSocketInterface[] = Object.values(this.Io.sockets.sockets) as ExSocketInterface[];
for (let i = 0; i < clients.length; i++) {
const client = clients[i];
@ -131,7 +131,7 @@ export class IoSocketController {
return client;
}
return null;
}
}*/
private authenticate(ws: WebSocket) {
//console.log(socket.handshake.query.token);
@ -657,25 +657,23 @@ export class IoSocketController {
//socket.emit(SocketIoEvent.GROUP_CREATE_UPDATE, groupUpdateMessage.serializeBinary().buffer);
}
private emitDeleteGroupEvent(socket: Socket, groupId: number): void {
private emitDeleteGroupEvent(client: ExSocketInterface, groupId: number): void {
const groupDeleteMessage = new GroupDeleteMessage();
groupDeleteMessage.setGroupid(groupId);
const subMessage = new SubMessage();
subMessage.setGroupdeletemessage(groupDeleteMessage);
const client : ExSocketInterface = socket as ExSocketInterface;
emitInBatch(client, subMessage);
}
private emitUserLeftEvent(socket: Socket, userId: number): void {
private emitUserLeftEvent(client: ExSocketInterface, userId: number): void {
const userLeftMessage = new UserLeftMessage();
userLeftMessage.setUserid(userId);
const subMessage = new SubMessage();
subMessage.setUserleftmessage(userLeftMessage);
const client : ExSocketInterface = socket as ExSocketInterface;
emitInBatch(client, subMessage);
}

View file

@ -1,18 +1,9 @@
import { readdirSync, statSync } from 'fs';
import { join, relative } from 'path';
import { Readable } from 'stream';
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js';
//import { watch } from 'chokidar';
import { wsConfig } from './livereload';
import sendFile from './sendfile';
import formData from './formdata';
import loadroutes from './loadroutes';
import { graphqlPost, graphqlWs } from './graphql';
import { stob } from './utils';
import { SendFileOptions, Handler } from './types';
import { Handler } from './types';
const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];
const noOp = () => true;
const handleBody = (res: HttpResponse, req: HttpRequest) => {
@ -24,7 +15,7 @@ const handleBody = (res: HttpResponse, req: HttpRequest) => {
this.onData((ab, isLast) => {
// uint and then slicing is bit faster than slice and then uint
stream.push(new Uint8Array(ab.slice(ab.byteOffset, ab.byteLength)));
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength)));
if (isLast) {
stream.push(null);
}
@ -37,15 +28,10 @@ const handleBody = (res: HttpResponse, req: HttpRequest) => {
if (contType.indexOf('application/json') > -1)
res.json = async () => JSON.parse(await res.body());
if (contTypes.map(t => contType.indexOf(t) > -1).indexOf(true) > -1)
res.formData = formData.bind(res, contType);
};
class BaseApp {
_staticPaths = new Map();
//_watched = new Map();
_sockets = new Map();
__livereloadenabled = false;
ws!: TemplatedApp['ws'];
get!: TemplatedApp['get'];
_post!: TemplatedApp['post'];
@ -53,84 +39,6 @@ class BaseApp {
_patch!: TemplatedApp['patch'];
_listen!: TemplatedApp['listen'];
file(pattern: string, filePath: string, options: SendFileOptions = {}) {
pattern=pattern.replace(/\\/g,'/');
if (this._staticPaths.has(pattern)) {
if (options.failOnDuplicateRoute)
throw Error(
`Error serving '${filePath}' for '${pattern}', already serving '${
this._staticPaths.get(pattern)[0]
}' file for this pattern.`
);
else if (!options.overwriteRoute) return this;
}
if (options.livereload && !this.__livereloadenabled) {
this.ws('/__sifrrLiveReload', wsConfig);
this.file('/livereload.js', join(__dirname, './livereloadjs.js'));
this.__livereloadenabled = true;
}
this._staticPaths.set(pattern, [filePath, options]);
this.get(pattern, this._serveStatic);
return this;
}
folder(prefix: string, folder: string, options: SendFileOptions, base: string = folder) {
// not a folder
if (!statSync(folder).isDirectory()) {
throw Error('Given path is not a directory: ' + folder);
}
// ensure slash in beginning and no trailing slash for prefix
if (prefix[0] !== '/') prefix = '/' + prefix;
if (prefix[prefix.length - 1] === '/') prefix = prefix.slice(0, -1);
// serve folder
const filter = options ? options.filter || noOp : noOp;
readdirSync(folder).forEach(file => {
// Absolute path
const filePath = join(folder, file);
// Return if filtered
if (!filter(filePath)) return;
if (statSync(filePath).isDirectory()) {
// Recursive if directory
this.folder(prefix, filePath, options, base);
} else {
this.file(prefix + '/' + relative(base, filePath), filePath, options);
}
});
/*if (options && options.watch) {
if (!this._watched.has(folder)) {
const w = watch(folder);
w.on('unlink', filePath => {
const url = '/' + relative(base, filePath);
this._staticPaths.delete(prefix + url);
});
w.on('add', filePath => {
const url = '/' + relative(base, filePath);
this.file(prefix + url, filePath, options);
});
this._watched.set(folder, w);
}
}*/
return this;
}
_serveStatic(res: HttpResponse, req: HttpRequest) {
res.onAborted(noOp);
const options = this._staticPaths.get(req.getUrl());
if (typeof options === 'undefined') {
res.writeStatus('404 Not Found');
res.end();
} else sendFile(res, req, options[0], options[1]);
}
post(pattern: string, handler: Handler) {
if (typeof handler !== 'function')
throw Error(`handler should be a function, given ${typeof handler}.`);
@ -163,21 +71,6 @@ class BaseApp {
return this;
}
graphql(route: string, schema, graphqlOptions: any = {}, uwsOptions = {}, graphql) {
const handler = graphqlPost(schema, graphqlOptions, graphql);
this.post(route, handler);
this.ws(route, graphqlWs(schema, graphqlOptions, uwsOptions, graphql));
// this.get(route, handler);
if (graphqlOptions && graphqlOptions.graphiqlPath)
this.file(graphqlOptions.graphiqlPath, join(__dirname, './graphiql.html'));
return this;
}
load(dir: string, options) {
loadroutes.call(this, dir, options);
return this;
}
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
if (typeof p === 'number' && typeof h === 'string') {
this._listen(h, p, socket => {
@ -202,8 +95,6 @@ class BaseApp {
}
close(port: null | number = null) {
//this._watched.forEach(v => v.close());
//this._watched.clear();
if (port) {
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
this._sockets.delete(port);

View file

@ -1,48 +0,0 @@
const noop = (a, b) => {};
export default class Cluster {
apps: any[];
listens = {};
// apps = [ { app: SifrrServerApp, port/ports: int } ]
constructor(apps) {
if (!Array.isArray(apps)) apps = [apps];
this.apps = apps;
}
listen(onListen = noop) {
for (let i = 0; i < this.apps.length; i++) {
const config = this.apps[i];
let { app, port, ports } = config;
if (!Array.isArray(ports) || ports.length === 0) {
ports = [port];
}
ports.forEach(p => {
if (typeof p !== 'number') throw Error(`Port should be a number, given ${p}`);
if (this.listens[p]) return;
app.listen(p, socket => {
onListen.call(app, socket, p);
});
this.listens[p] = app;
});
}
return this;
}
closeAll() {
Object.keys(this.listens).forEach(port => {
this.close(port);
});
return this;
}
close(port = null) {
if (port) {
this.listens[port] && this.listens[port].close(port);
delete this.listens[port];
} else {
this.closeAll();
}
return this;
}
}

View file

@ -1,99 +0,0 @@
import { createWriteStream } from 'fs';
import { join, dirname } from 'path';
import Busboy from 'busboy';
import mkdirp from 'mkdirp';
function formData(
contType: string,
options: busboy.BusboyConfig & {
abortOnLimit?: boolean;
tmpDir?: string;
onFile?: (
fieldname: string,
file: NodeJS.ReadableStream,
filename: string,
encoding: string,
mimetype: string
) => string;
onField?: (fieldname: string, value: any) => void;
filename?: (oldName: string) => string;
} = {}
) {
options.headers = {
'content-type': contType
};
return new Promise((resolve, reject) => {
const busb = new Busboy(options);
const ret = {};
this.bodyStream().pipe(busb);
busb.on('limit', () => {
if (options.abortOnLimit) {
reject(Error('limit'));
}
});
busb.on('file', function(fieldname, file, filename, encoding, mimetype) {
const value = {
filename,
encoding,
mimetype,
filePath: undefined
};
if (typeof options.tmpDir === 'string') {
if (typeof options.filename === 'function') filename = options.filename(filename);
const fileToSave = join(options.tmpDir, filename);
mkdirp(dirname(fileToSave));
file.pipe(createWriteStream(fileToSave));
value.filePath = fileToSave;
}
if (typeof options.onFile === 'function') {
value.filePath =
options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
}
setRetValue(ret, fieldname, value);
});
busb.on('field', function(fieldname, value) {
if (typeof options.onField === 'function') options.onField(fieldname, value);
setRetValue(ret, fieldname, value);
});
busb.on('finish', function() {
resolve(ret);
});
busb.on('error', reject);
});
}
function setRetValue(
ret: { [x: string]: any },
fieldname: string,
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any
) {
if (fieldname.slice(-2) === '[]') {
fieldname = fieldname.slice(0, fieldname.length - 2);
if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value);
} else {
ret[fieldname] = [value];
}
} else {
if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value);
} else if (ret[fieldname]) {
ret[fieldname] = [ret[fieldname], value];
} else {
ret[fieldname] = value;
}
}
}
export default formData;

View file

@ -1,133 +0,0 @@
<!--
* Copyright (c) 2019 GraphQL Contributors
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
-->
<!DOCTYPE html>
<html>
<head>
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
#graphiql {
height: 100vh;
}
</style>
<!--
This GraphiQL example depends on Promise and fetch, which are available in
modern browsers, but can be "polyfilled" for older browsers.
GraphiQL itself depends on React DOM.
If you do not want to rely on a CDN, you can host these files locally or
include them directly in your favored resource bunder.
-->
<script src="//cdn.jsdelivr.net/react/15.4.2/react.min.js"></script>
<script src="//cdn.jsdelivr.net/react/15.4.2/react-dom.min.js"></script>
<!--
These two files can be found in the npm module, however you may wish to
copy them directly into your environment, or perhaps include them in your
favored resource bundler.
-->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphiql/graphiql.css" />
<script src="https://cdn.jsdelivr.net/npm/graphiql/graphiql.js" charset="utf-8"></script>
<script src="https://cdn.jsdelivr.net/npm/@sifrr/fetch" charset="utf-8"></script>
</head>
<body>
<div id="graphiql">Loading...</div>
<script>
/**
* This GraphiQL example illustrates how to use some of GraphiQL's props
* in order to enable reading and updating the URL parameters, making
* link sharing of queries a little bit easier.
*
* This is only one example of this kind of feature, GraphiQL exposes
* various React params to enable interesting integrations.
*/
// Parse the search string to get url parameters.
var search = window.location.search;
var parameters = {};
search
.substr(1)
.split('&')
.forEach(function(entry) {
var eq = entry.indexOf('=');
if (eq >= 0) {
parameters[decodeURIComponent(entry.slice(0, eq))] = decodeURIComponent(
entry.slice(eq + 1)
);
}
});
// if variables was provided, try to format it.
if (parameters.variables) {
try {
parameters.variables = JSON.stringify(JSON.parse(parameters.variables), null, 2);
} catch (e) {
// Do nothing, we want to display the invalid JSON as a string, rather
// than present an error.
}
}
// When the query and variables string is edited, update the URL bar so
// that it can be easily shared
function onEditQuery(newQuery) {
parameters.query = newQuery;
updateURL();
}
function onEditVariables(newVariables) {
parameters.variables = newVariables;
updateURL();
}
function onEditOperationName(newOperationName) {
parameters.operationName = newOperationName;
updateURL();
}
function updateURL() {
var newSearch =
'?' +
Object.keys(parameters)
.filter(function(key) {
return Boolean(parameters[key]);
})
.map(function(key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(parameters[key]);
})
.join('&');
history.replaceState(null, null, newSearch);
}
// Defines a GraphQL fetcher using the fetch API. You're not required to
// use fetch, and could instead implement graphQLFetcher however you like,
// as long as it returns a Promise or Observable.
function graphQLFetcher(graphQLParams) {
// When working locally, the example expects a GraphQL server at the path /graphql.
// In a PR preview, it connects to the Star Wars API externally.
// Change this to point wherever you host your GraphQL server.
const api = '/graphql';
return Sifrr.Fetch.graphql(api, {
...graphQLParams
});
}
// Render <GraphiQL /> into the body.
// See the README in the top level of this module to learn more about
// how you can customize GraphiQL by providing different values or
// additional child elements.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
query: parameters.query,
variables: parameters.variables,
operationName: parameters.operationName,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName
}),
document.getElementById('graphiql')
);
</script>
</body>
</html>

View file

@ -1,138 +0,0 @@
import { parse } from 'query-string';
import { createAsyncIterator, forAwaitEach, isAsyncIterable } from 'iterall';
import { HttpResponse, HttpRequest } from 'uWebSockets.js';
// client -> server
const GQL_START = 'start';
const GQL_STOP = 'stop';
// server -> client
const GQL_DATA = 'data';
const GQL_QUERY = 'query';
async function getGraphqlParams(res: HttpResponse, req: HttpRequest) {
// query and variables
const queryParams = parse(req.getQuery());
let { query, variables, operationName } = queryParams;
if (typeof variables === 'string') variables = JSON.parse(variables);
// body
if (res && typeof res.json === 'function') {
const data = await res.json();
query = data.query || query;
variables = data.variables || variables;
operationName = data.operationName || operationName;
}
return {
source: query,
variableValues: variables,
operationName
};
}
function graphqlPost(schema, graphqlOptions: any = {}, graphql: any = {}) {
const execute = graphql.graphql || require('graphql').graphql;
return async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(console.error);
res.writeHeader('content-type', 'application/json');
res.end(
JSON.stringify(
await execute({
schema,
...(await getGraphqlParams(res, req)),
...graphqlOptions,
contextValue: {
res,
req,
...(graphqlOptions &&
(graphqlOptions.contextValue ||
(graphqlOptions.contextFxn && (await graphqlOptions.contextFxn(res, req)))))
}
})
)
);
};
}
function stopGqsSubscription(operations, reqOpId) {
if (!reqOpId) return;
operations[reqOpId] && operations[reqOpId].return && operations[reqOpId].return();
delete operations[reqOpId];
}
function graphqlWs(schema, graphqlOptions: any = {}, uwsOptions: any = {}, graphql: any = {}) {
const subscribe = graphql.subscribe || require('graphql').subscribe;
const execute = graphql.graphql || require('graphql').graphql;
return {
open: (ws, req) => {
ws.req = req;
ws.operations = {};
ws.opId = 1;
},
message: async (ws, message) => {
const { type, payload = {}, id: reqOpId } = JSON.parse(Buffer.from(message).toString('utf8'));
let opId;
if (reqOpId) {
opId = reqOpId;
} else {
opId = ws.opId++;
}
const params = {
schema,
source: payload.query,
variableValues: payload.variables,
operationName: payload.operationName,
contextValue: {
ws,
...(graphqlOptions &&
(graphqlOptions.contextValue ||
(graphqlOptions.contextFxn && (await graphqlOptions.contextFxn(ws)))))
},
...graphqlOptions
};
switch (type) {
case GQL_START:
stopGqsSubscription(ws.operations, opId);
// eslint-disable-next-line no-case-declarations
let asyncIterable = await subscribe(
params.schema,
graphql.parse(params.source),
params.rootValue,
params.contextValue,
params.variableValues,
params.operationName
);
asyncIterable = isAsyncIterable(asyncIterable)
? asyncIterable
: createAsyncIterator([asyncIterable]);
forAwaitEach(asyncIterable, result =>
ws.send(
JSON.stringify({
id: opId,
type: GQL_DATA,
payload: result
})
)
);
break;
case GQL_STOP:
stopGqsSubscription(ws.operations, reqOpId);
break;
default:
ws.send(JSON.stringify({ payload: await execute(params), type: GQL_QUERY, id: opId }));
break;
}
},
idleTimeout: 24 * 60 * 60,
...uwsOptions
};
}
export { graphqlPost, graphqlWs };

View file

@ -1,35 +0,0 @@
import { WebSocketBehavior, WebSocket } from 'uWebSockets.js';
const websockets = {};
let id = 0;
const wsConfig: WebSocketBehavior = {
open: (ws: WebSocket & { id: number }, req) => {
websockets[id] = {
dirty: false
};
ws.id = id;
console.log('websocket connected: ', id);
id++;
},
message: ws => {
ws.send(JSON.stringify(websockets[ws.id].dirty));
websockets[ws.id].dirty = false;
},
close: (ws, code, message) => {
delete websockets[ws.id];
console.log(
`websocket disconnected with code ${code} and message ${message}:`,
ws.id,
websockets
);
}
};
const sendSignal = (type: string, path: string) => {
console.log(type, 'signal for file: ', path);
for (let i in websockets) websockets[i].dirty = true;
};
export default { websockets, wsConfig, sendSignal };
export { websockets, wsConfig, sendSignal };

View file

@ -1,47 +0,0 @@
const loc = window.location;
let path;
if (loc.protocol === 'https:') {
path = 'wss:';
} else {
path = 'ws:';
}
path += '//' + loc.host + '/__sifrrLiveReload';
let ws,
ttr = 500,
timeout;
function newWsConnection() {
ws = new WebSocket(path);
ws.onopen = function() {
ttr = 500;
checkMessage();
console.log('watching for file changes through sifrr-server livereload mode.');
};
ws.onmessage = function(event) {
if (JSON.parse(event.data)) {
console.log('Files changed, refreshing page.');
location.reload();
}
};
ws.onerror = e => {
console.error('Webosocket error: ', e);
console.log('Retrying after ', ttr / 4, 'ms');
ttr *= 4;
};
ws.onclose = e => {
console.error(`Webosocket closed with code \${e.code} error \${e.message}`);
};
}
function checkMessage() {
if (!ws) return;
if (ws.readyState === WebSocket.OPEN) ws.send('');
else if (ws.readyState === WebSocket.CLOSED) newWsConnection();
if (timeout) clearTimeout(timeout);
timeout = setTimeout(checkMessage, ttr);
}
newWsConnection();
setTimeout(checkMessage, ttr);

View file

@ -1,42 +0,0 @@
import { statSync, readdirSync } from 'fs';
import { join, extname } from 'path';
function loadRoutes(dir, { filter = () => true, basePath = '' } = {}) {
let files;
const paths = [];
if (statSync(dir).isDirectory()) {
files = readdirSync(dir)
.filter(filter)
.map(file => join(dir, file));
} else {
files = [dir];
}
files.forEach(file => {
if (statSync(file).isDirectory()) {
// Recursive if directory
paths.push(...loadRoutes.call(this, file, { filter, basePath }));
} else if (extname(file) === '.js') {
const routes = require(file);
let basePaths = routes.basePath || [''];
delete routes.basePath;
if (typeof basePaths === 'string') basePaths = [basePaths];
basePaths.forEach(basep => {
for (const method in routes) {
const methodRoutes = routes[method];
for (let r in methodRoutes) {
if (!Array.isArray(methodRoutes[r])) methodRoutes[r] = [methodRoutes[r]];
this[method](basePath + basep + r, ...methodRoutes[r]);
paths.push(basePath + basep + r);
}
}
});
}
});
return paths;
}
export default loadRoutes;

View file

@ -1,176 +0,0 @@
const mimes = {
'3gp': 'video/3gpp',
a: 'application/octet-stream',
ai: 'application/postscript',
aif: 'audio/x-aiff',
aiff: 'audio/x-aiff',
asc: 'application/pgp-signature',
asf: 'video/x-ms-asf',
asm: 'text/x-asm',
asx: 'video/x-ms-asf',
atom: 'application/atom+xml',
au: 'audio/basic',
avi: 'video/x-msvideo',
bat: 'application/x-msdownload',
bin: 'application/octet-stream',
bmp: 'image/bmp',
bz2: 'application/x-bzip2',
c: 'text/x-c',
cab: 'application/vnd.ms-cab-compressed',
cc: 'text/x-c',
chm: 'application/vnd.ms-htmlhelp',
class: 'application/octet-stream',
com: 'application/x-msdownload',
conf: 'text/plain',
cpp: 'text/x-c',
crt: 'application/x-x509-ca-cert',
css: 'text/css',
csv: 'text/csv',
cxx: 'text/x-c',
deb: 'application/x-debian-package',
der: 'application/x-x509-ca-cert',
diff: 'text/x-diff',
djv: 'image/vnd.djvu',
djvu: 'image/vnd.djvu',
dll: 'application/x-msdownload',
dmg: 'application/octet-stream',
doc: 'application/msword',
dot: 'application/msword',
dtd: 'application/xml-dtd',
dvi: 'application/x-dvi',
ear: 'application/java-archive',
eml: 'message/rfc822',
eps: 'application/postscript',
exe: 'application/x-msdownload',
f: 'text/x-fortran',
f77: 'text/x-fortran',
f90: 'text/x-fortran',
flv: 'video/x-flv',
for: 'text/x-fortran',
gem: 'application/octet-stream',
gemspec: 'text/x-script.ruby',
gif: 'image/gif',
gz: 'application/x-gzip',
h: 'text/x-c',
hh: 'text/x-c',
htm: 'text/html',
html: 'text/html',
ico: 'image/vnd.microsoft.icon',
ics: 'text/calendar',
ifb: 'text/calendar',
iso: 'application/octet-stream',
jar: 'application/java-archive',
java: 'text/x-java-source',
jnlp: 'application/x-java-jnlp-file',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
js: 'application/javascript',
json: 'application/json',
log: 'text/plain',
m3u: 'audio/x-mpegurl',
m4v: 'video/mp4',
man: 'text/troff',
mathml: 'application/mathml+xml',
mbox: 'application/mbox',
mdoc: 'text/troff',
me: 'text/troff',
mid: 'audio/midi',
midi: 'audio/midi',
mime: 'message/rfc822',
mjs: 'application/javascript',
mml: 'application/mathml+xml',
mng: 'video/x-mng',
mov: 'video/quicktime',
mp3: 'audio/mpeg',
mp4: 'video/mp4',
mp4v: 'video/mp4',
mpeg: 'video/mpeg',
mpg: 'video/mpeg',
ms: 'text/troff',
msi: 'application/x-msdownload',
odp: 'application/vnd.oasis.opendocument.presentation',
ods: 'application/vnd.oasis.opendocument.spreadsheet',
odt: 'application/vnd.oasis.opendocument.text',
ogg: 'application/ogg',
p: 'text/x-pascal',
pas: 'text/x-pascal',
pbm: 'image/x-portable-bitmap',
pdf: 'application/pdf',
pem: 'application/x-x509-ca-cert',
pgm: 'image/x-portable-graymap',
pgp: 'application/pgp-encrypted',
pkg: 'application/octet-stream',
pl: 'text/x-script.perl',
pm: 'text/x-script.perl-module',
png: 'image/png',
pnm: 'image/x-portable-anymap',
ppm: 'image/x-portable-pixmap',
pps: 'application/vnd.ms-powerpoint',
ppt: 'application/vnd.ms-powerpoint',
ps: 'application/postscript',
psd: 'image/vnd.adobe.photoshop',
py: 'text/x-script.python',
qt: 'video/quicktime',
ra: 'audio/x-pn-realaudio',
rake: 'text/x-script.ruby',
ram: 'audio/x-pn-realaudio',
rar: 'application/x-rar-compressed',
rb: 'text/x-script.ruby',
rdf: 'application/rdf+xml',
roff: 'text/troff',
rpm: 'application/x-redhat-package-manager',
rss: 'application/rss+xml',
rtf: 'application/rtf',
ru: 'text/x-script.ruby',
s: 'text/x-asm',
sgm: 'text/sgml',
sgml: 'text/sgml',
sh: 'application/x-sh',
sig: 'application/pgp-signature',
snd: 'audio/basic',
so: 'application/octet-stream',
svg: 'image/svg+xml',
svgz: 'image/svg+xml',
swf: 'application/x-shockwave-flash',
t: 'text/troff',
tar: 'application/x-tar',
tbz: 'application/x-bzip-compressed-tar',
tcl: 'application/x-tcl',
tex: 'application/x-tex',
texi: 'application/x-texinfo',
texinfo: 'application/x-texinfo',
text: 'text/plain',
tif: 'image/tiff',
tiff: 'image/tiff',
torrent: 'application/x-bittorrent',
tr: 'text/troff',
txt: 'text/plain',
vcf: 'text/x-vcard',
vcs: 'text/x-vcalendar',
vrml: 'model/vrml',
war: 'application/java-archive',
wav: 'audio/x-wav',
wma: 'audio/x-ms-wma',
wmv: 'video/x-ms-wmv',
wmx: 'video/x-ms-wmx',
wrl: 'model/vrml',
wsdl: 'application/wsdl+xml',
xbm: 'image/x-xbitmap',
xhtml: 'application/xhtml+xml',
xls: 'application/vnd.ms-excel',
xml: 'application/xml',
xpm: 'image/x-xpixmap',
xsl: 'application/xml',
xslt: 'application/xslt+xml',
yaml: 'text/yaml',
yml: 'text/yaml',
zip: 'application/zip',
default: 'text/html'
};
const getMime = (path: string): string => {
const i = path.lastIndexOf('.');
return mimes[path.substr(i + 1).toLowerCase()] || mimes['default'];
};
export { getMime, mimes };

View file

@ -1,172 +0,0 @@
import { watch, statSync, createReadStream } from 'fs';
import { createBrotliCompress, createGzip, createDeflate } from 'zlib';
const watchedPaths = new Set();
const compressions = {
br: createBrotliCompress,
gzip: createGzip,
deflate: createDeflate
};
import { writeHeaders } from './utils';
import { getMime } from './mime';
const bytes = 'bytes=';
import { stob } from './utils';
import { sendSignal } from './livereload';
import { SendFileOptions } from './types';
import { HttpResponse, HttpRequest } from 'uWebSockets.js';
function sendFile(res: HttpResponse, req: HttpRequest, path: string, options: SendFileOptions) {
if (options && options.livereload && !watchedPaths.has(path)) {
watchedPaths.add(path);
watch(path, sendSignal);
}
sendFileToRes(
res,
{
'if-modified-since': req.getHeader('if-modified-since'),
range: req.getHeader('range'),
'accept-encoding': req.getHeader('accept-encoding')
},
path,
options
);
}
function sendFileToRes(
res: HttpResponse,
reqHeaders: { [name: string]: string },
path: string,
{
lastModified = true,
headers = {},
compress = false,
compressionOptions = {
priority: ['gzip', 'br', 'deflate']
},
cache = false
}: { cache: any } & any = {}
) {
let { mtime, size } = statSync(path);
mtime.setMilliseconds(0);
const mtimeutc = mtime.toUTCString();
headers = Object.assign({}, headers);
// handling last modified
if (lastModified) {
// Return 304 if last-modified
if (reqHeaders['if-modified-since']) {
if (new Date(reqHeaders['if-modified-since']) >= mtime) {
res.writeStatus('304 Not Modified');
return res.end();
}
}
headers['last-modified'] = mtimeutc;
}
headers['content-type'] = getMime(path);
// write data
let start = 0,
end = size - 1;
if (reqHeaders.range) {
compress = false;
const parts = reqHeaders.range.replace(bytes, '').split('-');
start = parseInt(parts[0], 10);
end = parts[1] ? parseInt(parts[1], 10) : end;
headers['accept-ranges'] = 'bytes';
headers['content-range'] = `bytes ${start}-${end}/${size}`;
size = end - start + 1;
res.writeStatus('206 Partial Content');
}
// for size = 0
if (end < 0) end = 0;
let readStream = createReadStream(path, { start, end });
// Compression;
let compressed: boolean | string = false;
if (compress) {
const l = compressionOptions.priority.length;
for (let i = 0; i < l; i++) {
const type = compressionOptions.priority[i];
if (reqHeaders['accept-encoding'].indexOf(type) > -1) {
compressed = type;
const compressor = compressions[type](compressionOptions);
readStream.pipe(compressor);
readStream = compressor;
headers['content-encoding'] = compressionOptions.priority[i];
break;
}
}
}
res.onAborted(() => readStream.destroy());
writeHeaders(res, headers);
// check cache
if (cache) {
return cache.wrap(
`${path}_${mtimeutc}_${start}_${end}_${compressed}`,
cb => {
stob(readStream)
.then(b => cb(null, b))
.catch(cb);
},
{ ttl: 0 },
(err, buffer) => {
if (err) {
res.writeStatus('500 Internal server error');
res.end();
throw err;
}
res.end(buffer);
}
);
} else if (compressed) {
readStream.on('data', buffer => {
res.write(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
});
} else {
readStream.on('data', buffer => {
const chunk = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
lastOffset = res.getWriteOffset();
// First try
const [ok, done] = res.tryEnd(chunk, size);
if (done) {
readStream.destroy();
} else if (!ok) {
// pause because backpressure
readStream.pause();
// Save unsent chunk for later
res.ab = chunk;
res.abOffset = lastOffset;
// Register async handlers for drainage
res.onWritable(offset => {
const [ok, done] = res.tryEnd(res.ab.slice(offset - res.abOffset), size);
if (done) {
readStream.destroy();
} else if (ok) {
readStream.resume();
}
return ok;
});
}
});
}
readStream
.on('error', e => {
res.writeStatus('500 Internal server error');
res.end();
readStream.destroy();
throw e;
})
.on('end', () => {
res.end();
});
}
export default sendFile;

View file

@ -6,21 +6,6 @@ export type UwsApp = {
prototype: TemplatedApp;
};
export type SendFileOptions = {
failOnDuplicateRoute?: boolean;
overwriteRoute?: boolean;
watch?: boolean;
filter?: (path: string) => boolean;
livereload?: boolean;
lastModified?: boolean;
headers?: { [name: string]: string };
compress?: boolean;
compressionOptions?: {
priority?: 'gzip' | 'br' | 'deflate';
};
cache?: boolean;
};
export type Handler = (res: HttpResponse, req: HttpRequest) => void;
export {};

View file

@ -1,21 +1,6 @@
import { HttpResponse } from 'uWebSockets.js';
import { ReadStream } from 'fs';
function writeHeaders(
res: HttpResponse,
headers: { [name: string]: string } | string,
other?: string
) {
if (typeof headers === 'string') {
res.writeHeader(headers, other.toString());
} else {
for (const n in headers) {
res.writeHeader(n, headers[n].toString());
}
}
}
function extend(who: object, from: object, overwrite = true) {
function extend(who: any, from: any, overwrite = true) {
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(
Object.keys(from)
);
@ -31,7 +16,7 @@ function extend(who: object, from: object, overwrite = true) {
function stob(stream: ReadStream): Promise<Buffer> {
return new Promise(resolve => {
const buffers = [];
const buffers: Buffer[] = [];
stream.on('data', buffers.push.bind(buffers));
stream.on('end', () => {
@ -49,4 +34,4 @@ function stob(stream: ReadStream): Promise<Buffer> {
});
}
export { writeHeaders, extend, stob };
export { extend, stob };

View file

@ -2,29 +2,18 @@ import { parse } from 'query-string';
import { HttpRequest } from 'uWebSockets.js';
import App from './server/app';
import SSLApp from './server/sslapp';
import { mimes, getMime } from './server/mime';
import { writeHeaders } from './server/utils';
import sendFile from './server/sendfile';
import Cluster from './server/cluster';
import livereload from './server/livereload';
import * as types from './server/types';
const getQuery = (req: HttpRequest) => {
return parse(req.getQuery());
};
export { App, SSLApp, mimes, getMime, writeHeaders, sendFile, Cluster, livereload, getQuery };
export { App, SSLApp, getQuery };
export * from './server/types';
export default {
App,
SSLApp,
mimes,
getMime,
writeHeaders,
sendFile,
Cluster,
livereload,
getQuery,
...types
};