barehttp 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -28
- package/lib/context/execution.d.ts +7 -0
- package/lib/context/execution.js +14 -0
- package/lib/context/index.d.ts +10 -0
- package/lib/context/index.js +46 -0
- package/lib/env.d.ts +5 -0
- package/lib/env.js +5 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +3 -0
- package/lib/logger/index.d.ts +16 -0
- package/lib/logger/index.js +26 -0
- package/lib/logger/serializers.d.ts +28 -0
- package/lib/logger/serializers.js +78 -0
- package/lib/middlewares/cookies/cookie-manager.d.ts +25 -0
- package/lib/middlewares/cookies/cookie-manager.js +68 -0
- package/lib/middlewares/cookies/signer.d.ts +8 -0
- package/lib/middlewares/cookies/signer.js +25 -0
- package/lib/middlewares/cors/cors.d.ts +38 -0
- package/lib/middlewares/cors/cors.js +164 -0
- package/lib/request.d.ts +84 -0
- package/lib/request.js +260 -0
- package/lib/schemas/custom-schema.d.ts +32 -0
- package/lib/schemas/custom-schema.js +62 -0
- package/lib/schemas/dirty-tsm.d.ts +1 -0
- package/lib/schemas/dirty-tsm.js +199 -0
- package/lib/schemas/generator.d.ts +7 -0
- package/lib/schemas/generator.js +179 -0
- package/lib/schemas/helpers.d.ts +27 -0
- package/lib/schemas/helpers.js +40 -0
- package/lib/schemas/json-schema.d.ts +2 -0
- package/lib/schemas/json-schema.js +48 -0
- package/lib/schemas/openami-schema.d.ts +2 -0
- package/lib/schemas/openami-schema.js +59 -0
- package/lib/schemas/project.d.ts +1 -0
- package/lib/schemas/project.js +1 -0
- package/lib/server.d.ts +154 -0
- package/lib/server.js +396 -0
- package/lib/utils/content-type.d.ts +54 -0
- package/lib/utils/content-type.js +54 -0
- package/lib/utils/http-methods.d.ts +11 -0
- package/lib/utils/http-methods.js +9 -0
- package/lib/utils/index.d.ts +4 -0
- package/lib/utils/index.js +4 -0
- package/lib/utils/safe-json.d.ts +2 -0
- package/lib/utils/safe-json.js +18 -0
- package/lib/utils/status-codes.d.ts +339 -0
- package/lib/utils/status-codes.js +339 -0
- package/lib/utils/status-phrases.d.ts +338 -0
- package/lib/utils/status-phrases.js +339 -0
- package/lib/websocket.d.ts +36 -0
- package/lib/websocket.js +176 -0
- package/package.json +64 -32
- package/.eslintrc.js +0 -47
- package/.github/workflows/release.yml +0 -27
- package/.jest-setup.js +0 -1
- package/jest.config.js +0 -8
- package/prettier.config.js +0 -6
- package/src/context/context.test.ts +0 -30
- package/src/context/execution.ts +0 -17
- package/src/context/index.ts +0 -61
- package/src/env.ts +0 -5
- package/src/examples/bare-http.ts +0 -36
- package/src/examples/express.ts +0 -11
- package/src/examples/fastify.ts +0 -18
- package/src/index.ts +0 -4
- package/src/logger/index.ts +0 -67
- package/src/logger/serializers.test.ts +0 -186
- package/src/logger/serializers.ts +0 -109
- package/src/middlewares/cookies/cookie-manager.ts +0 -86
- package/src/middlewares/cookies/signer.ts +0 -30
- package/src/report.ts +0 -25
- package/src/request.test.ts +0 -143
- package/src/request.ts +0 -277
- package/src/server.integration.test.ts +0 -296
- package/src/server.middlewares.test.ts +0 -93
- package/src/server.routes.test.ts +0 -71
- package/src/server.ts +0 -450
- package/src/utils/content-type.ts +0 -59
- package/src/utils/index.ts +0 -2
- package/src/utils/safe-json.ts +0 -17
- package/src/utils/status-codes.ts +0 -339
- package/src/utils/status-phrases.ts +0 -339
- package/tsconfig.json +0 -24
package/lib/server.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { ServerOptions } from 'ws';
|
|
2
|
+
import { Ajv } from 'ajv';
|
|
3
|
+
import { BareRequest, CacheOpts } from './request.js';
|
|
4
|
+
import { CookiesManagerOptions } from './middlewares/cookies/cookie-manager.js';
|
|
5
|
+
import { HttpMethodsUnion, StatusCodesUnion } from './utils/index.js';
|
|
6
|
+
import { CorsOptions } from './middlewares/cors/cors.js';
|
|
7
|
+
import { WebSocketServer } from './websocket.js';
|
|
8
|
+
import { Server } from 'http';
|
|
9
|
+
type Middleware = (flow: BareRequest) => Promise<void> | void;
|
|
10
|
+
type Handler<H extends {
|
|
11
|
+
[key: string]: string | undefined;
|
|
12
|
+
}> = (flow: BareRequest<H>) => any;
|
|
13
|
+
type ErrorHandler = (err: any, flow: BareRequest, status?: StatusCodesUnion) => void;
|
|
14
|
+
type IP = `${number}.${number}.${number}.${number}`;
|
|
15
|
+
type RouteOpts<C> = {
|
|
16
|
+
disableCache?: C extends true ? C : undefined;
|
|
17
|
+
cache?: C extends true ? undefined : CacheOpts;
|
|
18
|
+
/**
|
|
19
|
+
* Request timeout handler in `ms`
|
|
20
|
+
*/
|
|
21
|
+
timeout?: number;
|
|
22
|
+
builtInRuntime?: {
|
|
23
|
+
output?: boolean;
|
|
24
|
+
};
|
|
25
|
+
middlewares?: Array<Middleware>;
|
|
26
|
+
};
|
|
27
|
+
type BareOptions<A extends IP> = {
|
|
28
|
+
/**
|
|
29
|
+
* Declare a global middlewares array
|
|
30
|
+
* Default: []
|
|
31
|
+
*/
|
|
32
|
+
middlewares?: Array<Middleware>;
|
|
33
|
+
/**
|
|
34
|
+
* Opt-out request body parsing (de-serialization)
|
|
35
|
+
* Default `false`
|
|
36
|
+
*/
|
|
37
|
+
doNotParseBody?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Opt-in to have a custom swagger per route generation
|
|
40
|
+
* Default `false`
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Opt-in to have a custom runtime JSON Schema checker per routes
|
|
44
|
+
* Default `false`
|
|
45
|
+
*/
|
|
46
|
+
enableSchemaValidation?: boolean;
|
|
47
|
+
serverPort?: number;
|
|
48
|
+
declaredRoutesPaths?: Array<string>;
|
|
49
|
+
/**
|
|
50
|
+
* Address to bind the web server to
|
|
51
|
+
* Default '0.0.0.0'
|
|
52
|
+
*/
|
|
53
|
+
serverAddress?: A | 'localhost';
|
|
54
|
+
setRandomPort?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Enable request context storage
|
|
57
|
+
* Default `false`
|
|
58
|
+
*/
|
|
59
|
+
context?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Enable request/response predefined logging
|
|
62
|
+
* Default `false`
|
|
63
|
+
*/
|
|
64
|
+
logging?: boolean;
|
|
65
|
+
errorHandlerMiddleware?: ErrorHandler;
|
|
66
|
+
/**
|
|
67
|
+
* Request time format in `seconds` or `milliseconds`
|
|
68
|
+
* Default - disabled
|
|
69
|
+
*/
|
|
70
|
+
requestTimeFormat?: 's' | 'ms';
|
|
71
|
+
/**
|
|
72
|
+
* Control over cookies.
|
|
73
|
+
* This will enable automatic cookies decoding
|
|
74
|
+
*/
|
|
75
|
+
cookies?: boolean;
|
|
76
|
+
cookiesOptions?: CookiesManagerOptions;
|
|
77
|
+
/**
|
|
78
|
+
* Log the resolved reverse DNS first hop for remote ip of the client (first proxy)
|
|
79
|
+
*/
|
|
80
|
+
reverseDns?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* WebSocket server exposure
|
|
83
|
+
*/
|
|
84
|
+
ws?: boolean;
|
|
85
|
+
wsOptions?: Omit<ServerOptions, 'host' | 'port' | 'server' | 'noServer'> & {
|
|
86
|
+
closeHandler?: (server: WebSocketServer) => Promise<void>;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Enable Cors
|
|
90
|
+
*/
|
|
91
|
+
cors?: boolean | CorsOptions;
|
|
92
|
+
};
|
|
93
|
+
type ExtractRouteParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? {
|
|
94
|
+
[K in Param | keyof ExtractRouteParams<Rest>]: string;
|
|
95
|
+
} : T extends `${string}:${infer Param}` ? {
|
|
96
|
+
[K in Param]: string;
|
|
97
|
+
} : {
|
|
98
|
+
[k: string]: string;
|
|
99
|
+
};
|
|
100
|
+
interface HandlerExposed<K> {
|
|
101
|
+
<R extends `/${string}`, C>(setUp: K extends 'declare' ? {
|
|
102
|
+
route: R;
|
|
103
|
+
options?: RouteOpts<C>;
|
|
104
|
+
handler: Handler<ExtractRouteParams<R>>;
|
|
105
|
+
methods: Array<HttpMethodsUnion>;
|
|
106
|
+
} : {
|
|
107
|
+
route: R;
|
|
108
|
+
options?: RouteOpts<C>;
|
|
109
|
+
handler: Handler<ExtractRouteParams<R>>;
|
|
110
|
+
}): BareServer<any> & Routes;
|
|
111
|
+
}
|
|
112
|
+
export type RouteReport = {
|
|
113
|
+
hits: number;
|
|
114
|
+
success: number;
|
|
115
|
+
fails: number;
|
|
116
|
+
};
|
|
117
|
+
export type Routes = {
|
|
118
|
+
[K in HttpMethodsUnion | 'declare']: HandlerExposed<K>;
|
|
119
|
+
};
|
|
120
|
+
export type BareHttpType<A extends IP = any> = BareServer<A> & Routes;
|
|
121
|
+
export declare class BareServer<A extends IP> {
|
|
122
|
+
#private;
|
|
123
|
+
private bareOptions;
|
|
124
|
+
server: Server;
|
|
125
|
+
ws?: WebSocketServer;
|
|
126
|
+
ajv?: Ajv;
|
|
127
|
+
route: Readonly<Routes>;
|
|
128
|
+
constructor(bareOptions?: BareOptions<A>);
|
|
129
|
+
private applyLaunchOptions;
|
|
130
|
+
private applyMiddlewares;
|
|
131
|
+
/**
|
|
132
|
+
* This handler is used in async generated middlewares runtime function
|
|
133
|
+
*/
|
|
134
|
+
private resolveMiddleware;
|
|
135
|
+
private setRoute;
|
|
136
|
+
private handleRoute;
|
|
137
|
+
private resolveResponse;
|
|
138
|
+
private encodeRoute;
|
|
139
|
+
private explodeRoute;
|
|
140
|
+
private basicErrorHandler;
|
|
141
|
+
private stopWs;
|
|
142
|
+
private attachGracefulHandlers;
|
|
143
|
+
private attachRoutesDeclarator;
|
|
144
|
+
get runtimeRoute(): Readonly<Routes>;
|
|
145
|
+
start(cb?: (address: string) => void): Promise<void>;
|
|
146
|
+
stop(cb?: (e?: Error) => void): Promise<void>;
|
|
147
|
+
loadRoutesSchemas(): void;
|
|
148
|
+
use(middleware: Middleware): this;
|
|
149
|
+
getMiddlewares(): Middleware[];
|
|
150
|
+
setCustomErrorHandler(eh: ErrorHandler): void;
|
|
151
|
+
getServerPort(): number;
|
|
152
|
+
getRoutes(): string[];
|
|
153
|
+
}
|
|
154
|
+
export { BareServer as BareHttp };
|
package/lib/server.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import Router from 'find-my-way';
|
|
2
|
+
import { Ajv } from 'ajv';
|
|
3
|
+
import { BareRequest } from './request.js';
|
|
4
|
+
import { logMe } from './logger/index.js';
|
|
5
|
+
import { context, enableContext, newContext } from './context/index.js';
|
|
6
|
+
import { HttpMethods, } from './utils/index.js';
|
|
7
|
+
import { Cors } from './middlewares/cors/cors.js';
|
|
8
|
+
import { WebSocketServer } from './websocket.js';
|
|
9
|
+
import { generateRouteSchema } from './schemas/generator.js';
|
|
10
|
+
import dns from 'dns';
|
|
11
|
+
import { createServer } from 'http';
|
|
12
|
+
export class BareServer {
|
|
13
|
+
bareOptions;
|
|
14
|
+
server;
|
|
15
|
+
ws;
|
|
16
|
+
ajv;
|
|
17
|
+
route = {};
|
|
18
|
+
#middlewares = [];
|
|
19
|
+
#routes = new Map();
|
|
20
|
+
#routesLib = new Map();
|
|
21
|
+
#router = Router({ ignoreTrailingSlash: true });
|
|
22
|
+
#errorHandler = this.basicErrorHandler;
|
|
23
|
+
#corsInstance;
|
|
24
|
+
#port = 3000;
|
|
25
|
+
#host = '0.0.0.0';
|
|
26
|
+
#globalMiddlewaresRun = (_) => _;
|
|
27
|
+
#routeMiddlewaresStore = new Map();
|
|
28
|
+
#routeRuntimeSchemas = new Map();
|
|
29
|
+
constructor(bareOptions = {}) {
|
|
30
|
+
this.bareOptions = bareOptions;
|
|
31
|
+
// init
|
|
32
|
+
this.server = createServer(this.#listener.bind(this));
|
|
33
|
+
this.attachGracefulHandlers();
|
|
34
|
+
this.attachRoutesDeclarator();
|
|
35
|
+
this.applyLaunchOptions();
|
|
36
|
+
this.loadRoutesSchemas();
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
#listener = (request, response) => {
|
|
40
|
+
const flow = new BareRequest(request, response, this.bareOptions);
|
|
41
|
+
// init and attach request uuid to the context
|
|
42
|
+
if (this.bareOptions.context) {
|
|
43
|
+
newContext('request');
|
|
44
|
+
context.current?.store.set('id', flow.ID.code);
|
|
45
|
+
}
|
|
46
|
+
// execute global middlewares on the request
|
|
47
|
+
this.applyMiddlewares(flow)
|
|
48
|
+
.catch((e) => {
|
|
49
|
+
this.#errorHandler(e, flow, 400);
|
|
50
|
+
})
|
|
51
|
+
.then(() => {
|
|
52
|
+
// if middlewares sent the response back, stop here
|
|
53
|
+
if (flow.sent)
|
|
54
|
+
return;
|
|
55
|
+
this.#router.lookup(flow._originalRequest, flow._originalResponse);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* This function generates previously defined middlewares for the sequential execution
|
|
60
|
+
*/
|
|
61
|
+
#writeMiddlewares = () => {
|
|
62
|
+
const lines = [];
|
|
63
|
+
let order = 0;
|
|
64
|
+
const maxOrder = this.#middlewares.length;
|
|
65
|
+
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
66
|
+
if (maxOrder > 0) {
|
|
67
|
+
while (order <= maxOrder - 1) {
|
|
68
|
+
lines.push(`if (flow.sent) return;`);
|
|
69
|
+
lines.push(`await this.resolveMiddleware(flow, ${order});`);
|
|
70
|
+
order++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const text = lines.join('\n');
|
|
74
|
+
this.#globalMiddlewaresRun = new AsyncFunction('flow', text);
|
|
75
|
+
};
|
|
76
|
+
applyLaunchOptions = () => {
|
|
77
|
+
const { bareOptions: bo } = this;
|
|
78
|
+
if (bo.setRandomPort) {
|
|
79
|
+
this.#port = undefined;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.#port = +(bo.serverPort || process.env.PORT || 3000);
|
|
83
|
+
}
|
|
84
|
+
this.#host = typeof bo.serverAddress === 'string' ? bo.serverAddress : '0.0.0.0';
|
|
85
|
+
// context setting
|
|
86
|
+
if (bo.context)
|
|
87
|
+
enableContext();
|
|
88
|
+
// ws attachment
|
|
89
|
+
if (bo.ws) {
|
|
90
|
+
this.ws = new WebSocketServer(this.server, bo.wsOptions);
|
|
91
|
+
}
|
|
92
|
+
// middlewares settings
|
|
93
|
+
if (bo.errorHandlerMiddleware) {
|
|
94
|
+
this.#errorHandler = bo.errorHandlerMiddleware;
|
|
95
|
+
}
|
|
96
|
+
if (this.bareOptions.cors) {
|
|
97
|
+
const corsOpts = typeof this.bareOptions.cors === 'object' ? this.bareOptions.cors : {};
|
|
98
|
+
this.#corsInstance = new Cors(corsOpts);
|
|
99
|
+
}
|
|
100
|
+
this.#middlewares.push(...(bo.middlewares || []));
|
|
101
|
+
};
|
|
102
|
+
async applyMiddlewares(flow) {
|
|
103
|
+
if (this.bareOptions.cors) {
|
|
104
|
+
this.resolveMiddleware(flow, 0, this.#corsInstance?.corsMiddleware.bind(this.#corsInstance));
|
|
105
|
+
}
|
|
106
|
+
if (this.bareOptions.doNotParseBody !== true) {
|
|
107
|
+
// invoke body stream consumption
|
|
108
|
+
await flow['readBody']();
|
|
109
|
+
}
|
|
110
|
+
// attach cookies middleware
|
|
111
|
+
if (this.bareOptions.cookies) {
|
|
112
|
+
flow['attachCookieManager'](this.bareOptions.cookiesOptions);
|
|
113
|
+
flow['populateCookies']();
|
|
114
|
+
}
|
|
115
|
+
// to test in cloud provider
|
|
116
|
+
// this should resolve the name of the first hop from the dns chain
|
|
117
|
+
if (this.bareOptions.reverseDns) {
|
|
118
|
+
const remoteClient = await dns.promises.reverse(flow.remoteIp);
|
|
119
|
+
flow['setRemoteClient'](remoteClient[0]);
|
|
120
|
+
}
|
|
121
|
+
if (this.#middlewares.length)
|
|
122
|
+
await this.#globalMiddlewaresRun(flow);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* This handler is used in async generated middlewares runtime function
|
|
126
|
+
*/
|
|
127
|
+
async resolveMiddleware(flow, order, middleware) {
|
|
128
|
+
try {
|
|
129
|
+
const toExecute = middleware || this.#middlewares[order];
|
|
130
|
+
const response = toExecute(flow);
|
|
131
|
+
if (response instanceof Promise)
|
|
132
|
+
await response;
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
this.#errorHandler(e, flow);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
setRoute(method, route, isRuntime, handler, opts) {
|
|
139
|
+
const encode = this.encodeRoute(method, route);
|
|
140
|
+
const handleFn = (req, _, routeParams) => {
|
|
141
|
+
this.handleRoute(req, checkParams(routeParams), handler, opts);
|
|
142
|
+
};
|
|
143
|
+
this.#routesLib.set(encode, handleFn);
|
|
144
|
+
if (isRuntime) {
|
|
145
|
+
this.#router.reset();
|
|
146
|
+
this.#routesLib.forEach((handlerFn, route) => {
|
|
147
|
+
const [m, r] = this.explodeRoute(route);
|
|
148
|
+
this.#router.on(m, r, handlerFn);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
this.#router.on(method, route, handleFn);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
handleRoute(req, routeParams, handle, routeOpts) {
|
|
156
|
+
const flow = req.flow;
|
|
157
|
+
if (!flow) {
|
|
158
|
+
throw new Error(`No flow been found to route this request, theres a sync mistake in the server.`); // should NEVER happen
|
|
159
|
+
}
|
|
160
|
+
// populate with route params
|
|
161
|
+
if (routeParams)
|
|
162
|
+
flow['setParams'](routeParams);
|
|
163
|
+
// apply possible route options
|
|
164
|
+
if (routeOpts) {
|
|
165
|
+
if (routeOpts.disableCache)
|
|
166
|
+
flow.disableCache();
|
|
167
|
+
if (routeOpts.cache)
|
|
168
|
+
flow.setCache(routeOpts.cache);
|
|
169
|
+
if (routeOpts.timeout)
|
|
170
|
+
flow['attachTimeout'](routeOpts.timeout);
|
|
171
|
+
}
|
|
172
|
+
// TODO: implement per route middlewares!
|
|
173
|
+
try {
|
|
174
|
+
const routeReturn = handle(flow);
|
|
175
|
+
if (flow.sent)
|
|
176
|
+
return;
|
|
177
|
+
if (routeReturn instanceof Promise) {
|
|
178
|
+
routeReturn
|
|
179
|
+
.then((result) => this.resolveResponse(flow, result, req.url, req.method?.toLowerCase(), routeOpts?.builtInRuntime?.output))
|
|
180
|
+
.catch((e) => {
|
|
181
|
+
this.#errorHandler(e, flow);
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.resolveResponse(flow, routeReturn, req.url, req.method?.toLowerCase(), routeOpts?.builtInRuntime?.output);
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
this.#errorHandler(e, flow);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
resolveResponse(flow, response, url, method, builtInRuntime) {
|
|
192
|
+
if (!builtInRuntime || !method || !url) {
|
|
193
|
+
flow.send(response);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const schema = this.#routeRuntimeSchemas.get(`${method}-${url}`);
|
|
197
|
+
const check = schema?.compiled(response);
|
|
198
|
+
if ((schema && check) || !schema)
|
|
199
|
+
flow.send(response);
|
|
200
|
+
else {
|
|
201
|
+
logMe.error('Response schema error!', {
|
|
202
|
+
method,
|
|
203
|
+
url,
|
|
204
|
+
errors: schema?.compiled.errors,
|
|
205
|
+
received: response,
|
|
206
|
+
});
|
|
207
|
+
flow
|
|
208
|
+
.status(500)
|
|
209
|
+
.send({ message: `Response schema error, please communicate to server administrator.` });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
encodeRoute(method, route) {
|
|
213
|
+
if (route.endsWith('/'))
|
|
214
|
+
route = route.slice(0, -1);
|
|
215
|
+
return `${method}?${route}`;
|
|
216
|
+
}
|
|
217
|
+
explodeRoute(route) {
|
|
218
|
+
return route.split('?');
|
|
219
|
+
}
|
|
220
|
+
basicErrorHandler(e, flow, status) {
|
|
221
|
+
flow.status(status ?? 500).json({ ...e, message: e.message, stack: e.stack });
|
|
222
|
+
}
|
|
223
|
+
async stopWs() {
|
|
224
|
+
if (!this.ws)
|
|
225
|
+
return;
|
|
226
|
+
if (this.bareOptions.wsOptions?.closeHandler) {
|
|
227
|
+
await this.bareOptions.wsOptions.closeHandler(this.ws);
|
|
228
|
+
}
|
|
229
|
+
this.ws._internal.close();
|
|
230
|
+
}
|
|
231
|
+
attachGracefulHandlers() {
|
|
232
|
+
const graceful = async (code = 0) => {
|
|
233
|
+
await this.stop();
|
|
234
|
+
process.exit(code);
|
|
235
|
+
};
|
|
236
|
+
// Stop graceful
|
|
237
|
+
process.on('uncaughtException', (err) => {
|
|
238
|
+
console.error(err);
|
|
239
|
+
graceful(1);
|
|
240
|
+
});
|
|
241
|
+
process.on('unhandledRejection', (err) => {
|
|
242
|
+
console.error(err);
|
|
243
|
+
});
|
|
244
|
+
process.on('SIGTERM', () => graceful(0));
|
|
245
|
+
process.on('SIGINT', () => graceful(0));
|
|
246
|
+
}
|
|
247
|
+
attachRoutesDeclarator() {
|
|
248
|
+
for (const method of [...Object.keys(HttpMethods), 'declare']) {
|
|
249
|
+
this.route[method] = (routeSetUp) => {
|
|
250
|
+
checkRouteSetUp(routeSetUp, method);
|
|
251
|
+
if (method === 'declare') {
|
|
252
|
+
for (const m of new Set(routeSetUp.methods))
|
|
253
|
+
this.setRoute(HttpMethods[m], routeSetUp.route, false, routeSetUp.handler, routeSetUp.options);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
this.setRoute(HttpMethods[method], routeSetUp.route, false, routeSetUp.handler, routeSetUp.options);
|
|
257
|
+
}
|
|
258
|
+
return this;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// ========= PUBLIC APIS ==========
|
|
263
|
+
// TODO: add working options setter
|
|
264
|
+
// setOption<B extends BareOptions<any>, O extends keyof B>(option: O, arg: B[O]) {
|
|
265
|
+
// throw new Error('Not implemented')
|
|
266
|
+
// }
|
|
267
|
+
get runtimeRoute() {
|
|
268
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
269
|
+
const self = this;
|
|
270
|
+
return new Proxy({}, {
|
|
271
|
+
get(_, key) {
|
|
272
|
+
if (typeof key === 'symbol')
|
|
273
|
+
return this;
|
|
274
|
+
if (!self.server?.listening) {
|
|
275
|
+
console.warn('Runtime route declaration can be done only while the server is running. Follow documentation for more details');
|
|
276
|
+
return this;
|
|
277
|
+
}
|
|
278
|
+
if ([...Object.keys(HttpMethods), 'declare'].includes(key)) {
|
|
279
|
+
return (routeSetUp) => {
|
|
280
|
+
checkRouteSetUp(routeSetUp, key);
|
|
281
|
+
if (key === 'declare') {
|
|
282
|
+
for (const m of new Set(routeSetUp.methods))
|
|
283
|
+
self.setRoute(HttpMethods[m], routeSetUp.route, true, routeSetUp.handler, routeSetUp.options);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
self.setRoute(HttpMethods[key], routeSetUp.route, true, routeSetUp.handler, routeSetUp.options);
|
|
287
|
+
}
|
|
288
|
+
return this;
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return this;
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
start(cb) {
|
|
296
|
+
this.#writeMiddlewares();
|
|
297
|
+
this.ws?.['_start']();
|
|
298
|
+
return new Promise((res) =>
|
|
299
|
+
// https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback
|
|
300
|
+
this.server.listen(this.#port, this.#host, undefined, () => {
|
|
301
|
+
cb?.(`http://0.0.0.0:${this.#port}`);
|
|
302
|
+
res();
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
async stop(cb) {
|
|
306
|
+
// TODO: to solve problem announcing to clients the disconnect
|
|
307
|
+
// for (const flow of this.#flows.values()) {
|
|
308
|
+
// if (!flow.sent) {
|
|
309
|
+
// flow.status(500);
|
|
310
|
+
// flow.send('Server terminated');
|
|
311
|
+
// }
|
|
312
|
+
// }
|
|
313
|
+
if (!this.ws)
|
|
314
|
+
await this.stopWs();
|
|
315
|
+
if (!this.server?.listening)
|
|
316
|
+
return;
|
|
317
|
+
await new Promise((res, rej) => {
|
|
318
|
+
this.server.close((e) => {
|
|
319
|
+
if (e) {
|
|
320
|
+
rej(e);
|
|
321
|
+
cb?.(e);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
cb?.();
|
|
325
|
+
res();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
loadRoutesSchemas() {
|
|
331
|
+
if (!this.bareOptions.enableSchemaValidation) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (this.bareOptions.declaredRoutesPaths?.length) {
|
|
335
|
+
this.ajv = new Ajv({ strict: true });
|
|
336
|
+
for (const path of this.bareOptions.declaredRoutesPaths) {
|
|
337
|
+
const schemas = generateRouteSchema(path);
|
|
338
|
+
for (const schema of schemas) {
|
|
339
|
+
this.#routeRuntimeSchemas.set(`${schema.methodName}-${schema.route}`, {
|
|
340
|
+
raw: schema.jsonSchema,
|
|
341
|
+
compiled: this.ajv.compile(schema.jsonSchema),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
use(middleware) {
|
|
348
|
+
this.#middlewares.push(middleware);
|
|
349
|
+
return this;
|
|
350
|
+
}
|
|
351
|
+
getMiddlewares() {
|
|
352
|
+
return this.#middlewares;
|
|
353
|
+
}
|
|
354
|
+
setCustomErrorHandler(eh) {
|
|
355
|
+
this.#errorHandler = eh;
|
|
356
|
+
}
|
|
357
|
+
getServerPort() {
|
|
358
|
+
return this.server.address().port;
|
|
359
|
+
}
|
|
360
|
+
getRoutes() {
|
|
361
|
+
return [...this.#routes.keys()];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function checkRouteSetUp(routeSetUp, key) {
|
|
365
|
+
if (typeof routeSetUp.route !== 'string') {
|
|
366
|
+
throw new TypeError(`A route path for the method ${key} is not a a string`);
|
|
367
|
+
}
|
|
368
|
+
else if (routeSetUp.route[0] !== '/') {
|
|
369
|
+
throw new SyntaxError(`A route path should start with '/' for route ${routeSetUp.route} for method ${key}`);
|
|
370
|
+
}
|
|
371
|
+
else if (routeSetUp.route[1] === '/') {
|
|
372
|
+
throw new SyntaxError(`Declared route ${routeSetUp.route} for method ${key} is not correct, review the syntax`);
|
|
373
|
+
}
|
|
374
|
+
else if (typeof routeSetUp.handler !== 'function') {
|
|
375
|
+
throw new TypeError(`Handler for the route ${routeSetUp.route} for method ${key} is not a function`);
|
|
376
|
+
}
|
|
377
|
+
else if (routeSetUp.options?.timeout &&
|
|
378
|
+
typeof routeSetUp.options.timeout !== 'number' &&
|
|
379
|
+
!Number.isFinite(routeSetUp.options.timeout)) {
|
|
380
|
+
throw new TypeError(`Only numeric values are valid per-route timeout, submitted ${routeSetUp.options.timeout}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function checkParams(params) {
|
|
384
|
+
if (!params || Object.keys(params).length === 0)
|
|
385
|
+
return params;
|
|
386
|
+
for (const [param, value] of Object.entries(params)) {
|
|
387
|
+
if (value === undefined)
|
|
388
|
+
continue;
|
|
389
|
+
if (/(\.\/)(\.\.)(\\.)/.test(decodeURI(value))) {
|
|
390
|
+
logMe.warn(`Param ${param} value ${value} was redacted because contained dangerous characters`);
|
|
391
|
+
param[param] = 'REDACTED';
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return params;
|
|
395
|
+
}
|
|
396
|
+
export { BareServer as BareHttp };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export declare const ContentType: {
|
|
2
|
+
readonly 'application/EDI-X12': "application/EDI-X12";
|
|
3
|
+
readonly 'application/EDIFACT': "application/EDIFACT";
|
|
4
|
+
readonly 'application/javascript': "application/javascript";
|
|
5
|
+
readonly 'application/octet-stream': "application/octet-stream";
|
|
6
|
+
readonly 'application/ogg': "application/ogg";
|
|
7
|
+
readonly 'application/pdf': "application/pdf";
|
|
8
|
+
readonly 'application/xhtml+xml': "application/xhtml+xml";
|
|
9
|
+
readonly 'application/x-shockwave-flash': "application/x-shockwave-flash";
|
|
10
|
+
readonly 'application/json': "application/json";
|
|
11
|
+
readonly 'application/ld+json': "application/ld+json";
|
|
12
|
+
readonly 'application/xml': "application/xml";
|
|
13
|
+
readonly 'application/zip': "application/zip";
|
|
14
|
+
readonly 'application/x-www-form-urlencoded': "application/x-www-form-urlencoded";
|
|
15
|
+
readonly 'audio/mpeg': "audio/mpeg";
|
|
16
|
+
readonly 'audio/x-ms-wma': "audio/x-ms-wma";
|
|
17
|
+
readonly 'audio/vnd.rn-realaudio': "audio/vnd.rn-realaudio";
|
|
18
|
+
readonly 'audio/x-wav': "audio/x-wav";
|
|
19
|
+
readonly 'image/gif': "image/gif";
|
|
20
|
+
readonly 'image/jpeg': "image/jpeg";
|
|
21
|
+
readonly 'image/png': "image/png";
|
|
22
|
+
readonly 'image/tiff': "image/tiff";
|
|
23
|
+
readonly 'image/vnd.microsoft.icon': "image/vnd.microsoft.icon";
|
|
24
|
+
readonly 'image/x-icon': "image/x-icon";
|
|
25
|
+
readonly 'image/vnd.djvu': "image/vnd.djvu";
|
|
26
|
+
readonly 'image/svg+xml': "image/svg+xml";
|
|
27
|
+
readonly 'multipart/mixed': "multipart/mixed";
|
|
28
|
+
readonly 'multipart/alternative': "multipart/alternative";
|
|
29
|
+
readonly 'multipart/related': "multipart/related";
|
|
30
|
+
readonly 'multipart/form-data': "multipart/form-data";
|
|
31
|
+
readonly 'text/css': "text/css";
|
|
32
|
+
readonly 'text/csv': "text/csv";
|
|
33
|
+
readonly 'text/html': "text/html";
|
|
34
|
+
readonly 'text/plain': "text/plain";
|
|
35
|
+
readonly 'text/xml': "text/xml";
|
|
36
|
+
readonly 'video/mpeg': "video/mpeg";
|
|
37
|
+
readonly 'video/mp4': "video/mp4";
|
|
38
|
+
readonly 'video/quicktime': "video/quicktime";
|
|
39
|
+
readonly 'video/x-ms-wmv': "video/x-ms-wmv";
|
|
40
|
+
readonly 'video/x-msvideo': "video/x-msvideo";
|
|
41
|
+
readonly 'video/x-flv': "video/x-flv";
|
|
42
|
+
readonly 'video/webm': "video/webm";
|
|
43
|
+
readonly 'application/vnd.oasis.opendocument.text': "application/vnd.oasis.opendocument.text";
|
|
44
|
+
readonly 'application/vnd.oasis.opendocument.spreadsheet': "application/vnd.oasis.opendocument.spreadsheet";
|
|
45
|
+
readonly 'application/vnd.oasis.opendocument.presentation': "application/vnd.oasis.opendocument.presentation";
|
|
46
|
+
readonly 'application/vnd.oasis.opendocument.graphics': "application/vnd.oasis.opendocument.graphics";
|
|
47
|
+
readonly 'application/vnd.ms-excel': "application/vnd.ms-excel";
|
|
48
|
+
readonly 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
49
|
+
readonly 'application/vnd.ms-powerpoint': "application/vnd.ms-powerpoint";
|
|
50
|
+
readonly 'application/vnd.openxmlformats-officedocument.presentationml.presentation': "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
51
|
+
readonly 'application/msword': "application/msword";
|
|
52
|
+
readonly 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
53
|
+
readonly 'application/vnd.mozilla.xul+xml': "application/vnd.mozilla.xul+xml";
|
|
54
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const ContentType = {
|
|
2
|
+
'application/EDI-X12': 'application/EDI-X12',
|
|
3
|
+
'application/EDIFACT': 'application/EDIFACT',
|
|
4
|
+
'application/javascript': 'application/javascript',
|
|
5
|
+
'application/octet-stream': 'application/octet-stream',
|
|
6
|
+
'application/ogg': 'application/ogg',
|
|
7
|
+
'application/pdf': 'application/pdf',
|
|
8
|
+
'application/xhtml+xml': 'application/xhtml+xml',
|
|
9
|
+
'application/x-shockwave-flash': 'application/x-shockwave-flash',
|
|
10
|
+
'application/json': 'application/json',
|
|
11
|
+
'application/ld+json': 'application/ld+json',
|
|
12
|
+
'application/xml': 'application/xml',
|
|
13
|
+
'application/zip': 'application/zip',
|
|
14
|
+
'application/x-www-form-urlencoded': 'application/x-www-form-urlencoded',
|
|
15
|
+
'audio/mpeg': 'audio/mpeg',
|
|
16
|
+
'audio/x-ms-wma': 'audio/x-ms-wma',
|
|
17
|
+
'audio/vnd.rn-realaudio': 'audio/vnd.rn-realaudio',
|
|
18
|
+
'audio/x-wav': 'audio/x-wav',
|
|
19
|
+
'image/gif': 'image/gif',
|
|
20
|
+
'image/jpeg': 'image/jpeg',
|
|
21
|
+
'image/png': 'image/png',
|
|
22
|
+
'image/tiff': 'image/tiff',
|
|
23
|
+
'image/vnd.microsoft.icon': 'image/vnd.microsoft.icon',
|
|
24
|
+
'image/x-icon': 'image/x-icon',
|
|
25
|
+
'image/vnd.djvu': 'image/vnd.djvu',
|
|
26
|
+
'image/svg+xml': 'image/svg+xml',
|
|
27
|
+
'multipart/mixed': 'multipart/mixed',
|
|
28
|
+
'multipart/alternative': 'multipart/alternative',
|
|
29
|
+
'multipart/related': 'multipart/related', // (using by MHTML (HTML mail).)
|
|
30
|
+
'multipart/form-data': 'multipart/form-data',
|
|
31
|
+
'text/css': 'text/css',
|
|
32
|
+
'text/csv': 'text/csv',
|
|
33
|
+
'text/html': 'text/html',
|
|
34
|
+
'text/plain': 'text/plain',
|
|
35
|
+
'text/xml': 'text/xml',
|
|
36
|
+
'video/mpeg': 'video/mpeg',
|
|
37
|
+
'video/mp4': 'video/mp4',
|
|
38
|
+
'video/quicktime': 'video/quicktime',
|
|
39
|
+
'video/x-ms-wmv': 'video/x-ms-wmv',
|
|
40
|
+
'video/x-msvideo': 'video/x-msvideo',
|
|
41
|
+
'video/x-flv': 'video/x-flv',
|
|
42
|
+
'video/webm': 'video/webm',
|
|
43
|
+
'application/vnd.oasis.opendocument.text': 'application/vnd.oasis.opendocument.text',
|
|
44
|
+
'application/vnd.oasis.opendocument.spreadsheet': 'application/vnd.oasis.opendocument.spreadsheet',
|
|
45
|
+
'application/vnd.oasis.opendocument.presentation': 'application/vnd.oasis.opendocument.presentation',
|
|
46
|
+
'application/vnd.oasis.opendocument.graphics': 'application/vnd.oasis.opendocument.graphics',
|
|
47
|
+
'application/vnd.ms-excel': 'application/vnd.ms-excel',
|
|
48
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
49
|
+
'application/vnd.ms-powerpoint': 'application/vnd.ms-powerpoint',
|
|
50
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
51
|
+
'application/msword': 'application/msword',
|
|
52
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
53
|
+
'application/vnd.mozilla.xul+xml': 'application/vnd.mozilla.xul+xml',
|
|
54
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const HttpMethods: {
|
|
2
|
+
readonly get: "GET";
|
|
3
|
+
readonly post: "POST";
|
|
4
|
+
readonly put: "PUT";
|
|
5
|
+
readonly delete: "DELETE";
|
|
6
|
+
readonly patch: "PATCH";
|
|
7
|
+
readonly options: "OPTIONS";
|
|
8
|
+
readonly head: "HEAD";
|
|
9
|
+
};
|
|
10
|
+
export type HttpMethodsUnion = keyof typeof HttpMethods;
|
|
11
|
+
export type HttpMethodsUnionUppercase = typeof HttpMethods[keyof typeof HttpMethods];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { StatusPhrases } from './status-phrases.js';
|
|
2
|
+
export { StatusCodes, StatusCodesUnion } from './status-codes.js';
|
|
3
|
+
export { HttpMethods, HttpMethodsUnion, HttpMethodsUnionUppercase } from './http-methods.js';
|
|
4
|
+
export { JSONStringify, JSONParse } from './safe-json.js';
|