barehttp 1.0.0 → 2.1.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 +65 -33
- 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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { serialize } from 'cookie';
|
|
2
|
+
import { secretsOperator } from './signer.js';
|
|
3
|
+
import { logMe } from '../../logger/index.js';
|
|
4
|
+
export class CookiesManager {
|
|
5
|
+
options;
|
|
6
|
+
flow;
|
|
7
|
+
signer;
|
|
8
|
+
constructor(options = {}, flow) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.flow = flow;
|
|
11
|
+
const secret = this.options.secret || '';
|
|
12
|
+
const enableRotation = Array.isArray(secret);
|
|
13
|
+
this.signer = typeof secret === 'string' || enableRotation ? secretsOperator(secret) : null;
|
|
14
|
+
}
|
|
15
|
+
setCookie(name, value, options, signer) {
|
|
16
|
+
const localSigner = signer || this.signer;
|
|
17
|
+
const opts = options || this.options;
|
|
18
|
+
const { signed, ...rest } = opts;
|
|
19
|
+
const normalizedExpires = typeof rest.expires === 'number' ? new Date(rest.expires) : rest.expires;
|
|
20
|
+
const serializeOptions = {
|
|
21
|
+
...rest,
|
|
22
|
+
expires: normalizedExpires,
|
|
23
|
+
};
|
|
24
|
+
if (signed && localSigner) {
|
|
25
|
+
value = localSigner.sign(value);
|
|
26
|
+
}
|
|
27
|
+
const serialized = serialize(name, value, serializeOptions);
|
|
28
|
+
let setCookie = this.flow.getHeader('Set-Cookie');
|
|
29
|
+
if (!setCookie) {
|
|
30
|
+
this.flow.setHeader('Set-Cookie', serialized);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (typeof setCookie === 'string') {
|
|
34
|
+
setCookie = [setCookie];
|
|
35
|
+
}
|
|
36
|
+
setCookie.push(serialized);
|
|
37
|
+
this.flow.setHeader('Set-Cookie', setCookie);
|
|
38
|
+
}
|
|
39
|
+
clearCookie(name, options = {}) {
|
|
40
|
+
const opts = {
|
|
41
|
+
path: '/',
|
|
42
|
+
...options,
|
|
43
|
+
expires: new Date(1),
|
|
44
|
+
signed: undefined,
|
|
45
|
+
maxAge: undefined,
|
|
46
|
+
};
|
|
47
|
+
return this.setCookie(name, '', opts);
|
|
48
|
+
}
|
|
49
|
+
parseCookie(rawCookie) {
|
|
50
|
+
if (!rawCookie)
|
|
51
|
+
return {};
|
|
52
|
+
const result = {};
|
|
53
|
+
const values = rawCookie?.split(';');
|
|
54
|
+
for (let i = 0; i < values.length - 1; i++) {
|
|
55
|
+
const split = values[i].trim().split('=');
|
|
56
|
+
if (split.length == 2)
|
|
57
|
+
result[split[0]] = split[1];
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
unsignCookie(value) {
|
|
62
|
+
if (!this.signer) {
|
|
63
|
+
logMe.error('No signer defined for the cookies, unsign wont work');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
return this.signer.unsign(value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import cookieSignature from 'cookie-signature';
|
|
2
|
+
export function secretsOperator(secret) {
|
|
3
|
+
const secrets = Array.isArray(secret) ? secret : [secret];
|
|
4
|
+
const [signingKey] = secrets;
|
|
5
|
+
return {
|
|
6
|
+
sign(value) {
|
|
7
|
+
return cookieSignature.sign(value, signingKey);
|
|
8
|
+
},
|
|
9
|
+
unsign(signedValue) {
|
|
10
|
+
let valid = false;
|
|
11
|
+
let renew = false;
|
|
12
|
+
let value = null;
|
|
13
|
+
for (const key of secrets) {
|
|
14
|
+
const result = cookieSignature.unsign(signedValue, key);
|
|
15
|
+
if (result !== false) {
|
|
16
|
+
valid = true;
|
|
17
|
+
renew = key !== signingKey;
|
|
18
|
+
value = result;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { valid, renew, value };
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { HttpMethodsUnionUppercase } from '../../utils/index.js';
|
|
2
|
+
import type { BareRequest } from '../../request.js';
|
|
3
|
+
export type CorsOptions = {
|
|
4
|
+
origin?: string | RegExp;
|
|
5
|
+
methods?: Array<HttpMethodsUnionUppercase>;
|
|
6
|
+
preflightContinue?: boolean;
|
|
7
|
+
optionsSuccessStatus?: 200 | 201 | 202 | 203 | 204;
|
|
8
|
+
allowedHeaders?: string | string[];
|
|
9
|
+
exposedHeaders?: string | string[];
|
|
10
|
+
credentials?: boolean;
|
|
11
|
+
maxAge?: number;
|
|
12
|
+
};
|
|
13
|
+
export declare class Cors {
|
|
14
|
+
options: CorsOptions;
|
|
15
|
+
defaults: {
|
|
16
|
+
origin: string;
|
|
17
|
+
methods: string[];
|
|
18
|
+
preflightContinue: boolean;
|
|
19
|
+
optionsSuccessStatus: number;
|
|
20
|
+
};
|
|
21
|
+
computedHeaders: {
|
|
22
|
+
credentials: {};
|
|
23
|
+
methods: {};
|
|
24
|
+
maxAge: {};
|
|
25
|
+
exposedHeaders: {};
|
|
26
|
+
};
|
|
27
|
+
constructor(corsOptions?: CorsOptions);
|
|
28
|
+
private computeStaticHeaders;
|
|
29
|
+
private isString;
|
|
30
|
+
private isOriginAllowed;
|
|
31
|
+
private configureOrigin;
|
|
32
|
+
private configureMethods;
|
|
33
|
+
private configureCredentials;
|
|
34
|
+
private configureAllowedHeaders;
|
|
35
|
+
private configureExposedHeaders;
|
|
36
|
+
private configureMaxAge;
|
|
37
|
+
corsMiddleware(flow: BareRequest): void;
|
|
38
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
export class Cors {
|
|
3
|
+
options;
|
|
4
|
+
defaults = {
|
|
5
|
+
origin: '*',
|
|
6
|
+
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
|
|
7
|
+
preflightContinue: false,
|
|
8
|
+
optionsSuccessStatus: 204,
|
|
9
|
+
};
|
|
10
|
+
computedHeaders = {
|
|
11
|
+
credentials: {},
|
|
12
|
+
methods: {},
|
|
13
|
+
maxAge: {},
|
|
14
|
+
exposedHeaders: {},
|
|
15
|
+
};
|
|
16
|
+
constructor(corsOptions = {}) {
|
|
17
|
+
this.options = Object.assign({}, this.defaults, corsOptions);
|
|
18
|
+
this.computeStaticHeaders();
|
|
19
|
+
}
|
|
20
|
+
computeStaticHeaders() {
|
|
21
|
+
this.computedHeaders = {
|
|
22
|
+
credentials: this.configureCredentials(),
|
|
23
|
+
methods: this.configureMethods(),
|
|
24
|
+
maxAge: this.configureMaxAge(),
|
|
25
|
+
exposedHeaders: this.configureExposedHeaders(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
isString(s) {
|
|
29
|
+
return typeof s === 'string' || s instanceof String;
|
|
30
|
+
}
|
|
31
|
+
isOriginAllowed(origin, allowedOrigin) {
|
|
32
|
+
if (Array.isArray(allowedOrigin)) {
|
|
33
|
+
for (let i = 0; i < allowedOrigin.length; ++i) {
|
|
34
|
+
if (this.isOriginAllowed(origin, allowedOrigin[i])) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
else if (this.isString(allowedOrigin)) {
|
|
41
|
+
return origin === allowedOrigin;
|
|
42
|
+
}
|
|
43
|
+
else if (allowedOrigin instanceof RegExp) {
|
|
44
|
+
return allowedOrigin.test(origin);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
return !!allowedOrigin;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
configureOrigin(req) {
|
|
51
|
+
const requestOrigin = req.headers.origin;
|
|
52
|
+
const headers = {};
|
|
53
|
+
let isAllowed;
|
|
54
|
+
if (!this.options.origin || this.options.origin === '*') {
|
|
55
|
+
// allow any origin
|
|
56
|
+
Object.assign(headers, { 'Access-Control-Allow-Origin': '*' });
|
|
57
|
+
}
|
|
58
|
+
else if (this.isString(this.options.origin)) {
|
|
59
|
+
// fixed origin
|
|
60
|
+
Object.assign(headers, {
|
|
61
|
+
'Access-Control-Allow-Origin': this.options.origin,
|
|
62
|
+
Vary: 'Origin',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
isAllowed = this.isOriginAllowed(requestOrigin, this.options.origin);
|
|
67
|
+
// reflect origin
|
|
68
|
+
Object.assign(headers, {
|
|
69
|
+
'Access-Control-Allow-Origin': isAllowed ? requestOrigin : false,
|
|
70
|
+
Vary: 'Origin',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return headers;
|
|
74
|
+
}
|
|
75
|
+
configureMethods() {
|
|
76
|
+
const { methods } = this.options;
|
|
77
|
+
return {
|
|
78
|
+
'Access-Control-Allow-Methods': methods,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
configureCredentials() {
|
|
82
|
+
if (this.options.credentials === true) {
|
|
83
|
+
return {
|
|
84
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
configureAllowedHeaders(req) {
|
|
90
|
+
let { allowedHeaders } = this.options;
|
|
91
|
+
const headers = {};
|
|
92
|
+
if (!allowedHeaders) {
|
|
93
|
+
allowedHeaders = req.headers['access-control-request-headers']; // .headers wasn't specified, so reflect the request headers
|
|
94
|
+
Object.assign(headers, {
|
|
95
|
+
Vary: 'Access-Control-Request-Headers',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
else if (Array.isArray(allowedHeaders)) {
|
|
99
|
+
allowedHeaders = allowedHeaders.join(','); // .headers is an array, so turn it into a string
|
|
100
|
+
}
|
|
101
|
+
if (allowedHeaders && allowedHeaders.length) {
|
|
102
|
+
Object.assign(headers, {
|
|
103
|
+
'Access-Control-Allow-Headers': allowedHeaders,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return headers;
|
|
107
|
+
}
|
|
108
|
+
configureExposedHeaders() {
|
|
109
|
+
let headers = this.options.exposedHeaders;
|
|
110
|
+
if (!headers) {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
else if (Array.isArray(headers)) {
|
|
114
|
+
headers = headers.join(','); // .headers is an array, so turn it into a string
|
|
115
|
+
}
|
|
116
|
+
if (headers && headers.length) {
|
|
117
|
+
return {
|
|
118
|
+
'Access-Control-Expose-Headers': headers,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
configureMaxAge() {
|
|
124
|
+
const maxAge = (typeof this.options.maxAge === 'number' || this.options.maxAge) &&
|
|
125
|
+
this.options.maxAge.toString();
|
|
126
|
+
if (maxAge && maxAge.length) {
|
|
127
|
+
return { 'Access-Control-Max-Age': maxAge };
|
|
128
|
+
}
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
corsMiddleware(flow) {
|
|
132
|
+
let headers = {};
|
|
133
|
+
const req = flow._originalRequest;
|
|
134
|
+
const method = req.method && req.method.toUpperCase && req.method.toUpperCase();
|
|
135
|
+
if (method === 'OPTIONS') {
|
|
136
|
+
// preflight
|
|
137
|
+
headers = {
|
|
138
|
+
...this.configureOrigin(req),
|
|
139
|
+
...this.configureAllowedHeaders(req),
|
|
140
|
+
...this.computedHeaders.credentials,
|
|
141
|
+
...this.computedHeaders.maxAge,
|
|
142
|
+
...this.computedHeaders.exposedHeaders,
|
|
143
|
+
...this.computedHeaders.methods,
|
|
144
|
+
};
|
|
145
|
+
flow.setHeaders(headers);
|
|
146
|
+
if (!this.options.preflightContinue) {
|
|
147
|
+
// Safari (and potentially other browsers) need content-length 0,
|
|
148
|
+
// for 204 or they just hang waiting for a body
|
|
149
|
+
flow.status(this.options.optionsSuccessStatus);
|
|
150
|
+
flow.setHeader('Content-Length', '0');
|
|
151
|
+
flow.send();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// actual response
|
|
156
|
+
headers = {
|
|
157
|
+
...this.configureOrigin(req),
|
|
158
|
+
...this.computedHeaders.credentials,
|
|
159
|
+
...this.computedHeaders.exposedHeaders,
|
|
160
|
+
};
|
|
161
|
+
flow.setHeaders(headers);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
package/lib/request.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { StatusCodesUnion } from './utils/index.js';
|
|
2
|
+
import { CookiesManager } from './middlewares/cookies/cookie-manager.js';
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
4
|
+
type Cacheability = 'public' | 'private' | 'no-cache' | 'no-store';
|
|
5
|
+
type ExpirationType = 'max-age' | 's-maxage' | 'max-stale' | 'min-fresh' | 'stale-while-revalidate' | 'stale-if-error';
|
|
6
|
+
type Revalidation = 'must-revalidate' | 'proxy-revalidate' | 'immutable';
|
|
7
|
+
export type CacheOpts = {
|
|
8
|
+
cacheability: Cacheability;
|
|
9
|
+
expirationKind: ExpirationType;
|
|
10
|
+
/**
|
|
11
|
+
* Default 3600
|
|
12
|
+
*/
|
|
13
|
+
expirationSeconds?: number;
|
|
14
|
+
revalidation?: Revalidation;
|
|
15
|
+
};
|
|
16
|
+
export declare class BareRequest<H extends {
|
|
17
|
+
[key: string]: string | undefined;
|
|
18
|
+
} = {
|
|
19
|
+
[key: string]: string | undefined;
|
|
20
|
+
}> {
|
|
21
|
+
_originalRequest: IncomingMessage;
|
|
22
|
+
_originalResponse: ServerResponse;
|
|
23
|
+
ID: {
|
|
24
|
+
code: string;
|
|
25
|
+
};
|
|
26
|
+
params: H;
|
|
27
|
+
query: {
|
|
28
|
+
[k: string]: string | undefined;
|
|
29
|
+
};
|
|
30
|
+
remoteIp?: string;
|
|
31
|
+
requestBody?: any;
|
|
32
|
+
requestHeaders: {
|
|
33
|
+
[key: string]: any;
|
|
34
|
+
};
|
|
35
|
+
statusToSend: number;
|
|
36
|
+
cm?: CookiesManager;
|
|
37
|
+
sent: boolean;
|
|
38
|
+
private cache;
|
|
39
|
+
private startTime?;
|
|
40
|
+
private startDate;
|
|
41
|
+
private remoteClient;
|
|
42
|
+
private logging;
|
|
43
|
+
private requestTimeFormat?;
|
|
44
|
+
private headers;
|
|
45
|
+
private cookies;
|
|
46
|
+
private contentType?;
|
|
47
|
+
private timeout?;
|
|
48
|
+
constructor(_originalRequest: IncomingMessage, _originalResponse: ServerResponse, options?: {
|
|
49
|
+
logging?: boolean;
|
|
50
|
+
requestTimeFormat?: 'ms' | 's';
|
|
51
|
+
});
|
|
52
|
+
private readBody;
|
|
53
|
+
private attachCookieManager;
|
|
54
|
+
private populateCookies;
|
|
55
|
+
private classifyRequestBody;
|
|
56
|
+
private setRemoteClient;
|
|
57
|
+
private setRequestTime;
|
|
58
|
+
private cleanHeader;
|
|
59
|
+
private attachTimeout;
|
|
60
|
+
private setParams;
|
|
61
|
+
getHeader(header: string): string | string[];
|
|
62
|
+
getCookie(cookie: string): string;
|
|
63
|
+
getCookies(): {
|
|
64
|
+
[cooke: string]: string;
|
|
65
|
+
};
|
|
66
|
+
disableCache(): void;
|
|
67
|
+
setCache(cacheOpts: CacheOpts): void;
|
|
68
|
+
addHeader(header: string, value: string | number | string[] | number[]): void;
|
|
69
|
+
setHeader(header: string, value: string | number | string[] | number[]): void;
|
|
70
|
+
setHeaders(headers: {
|
|
71
|
+
[header: string]: string | number | string[] | number[];
|
|
72
|
+
}): void;
|
|
73
|
+
addHeaders(headers: {
|
|
74
|
+
[header: string]: string | number | string[] | number[];
|
|
75
|
+
}): void;
|
|
76
|
+
status(status: StatusCodesUnion): this;
|
|
77
|
+
sendStatus(status: StatusCodesUnion): void;
|
|
78
|
+
stream<T extends NodeJS.WritableStream>(stream: T): void;
|
|
79
|
+
json(data: any): void;
|
|
80
|
+
_send(chunk?: string | ArrayBuffer | NodeJS.ArrayBufferView | SharedArrayBuffer): void;
|
|
81
|
+
sendStringifiedJson(data: string): void;
|
|
82
|
+
send(anything?: any): void;
|
|
83
|
+
}
|
|
84
|
+
export {};
|
package/lib/request.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import hyperid from 'hyperid';
|
|
2
|
+
import { StatusCodes, StatusPhrases } from './utils/index.js';
|
|
3
|
+
import { JSONParse, JSONStringify } from './utils/safe-json.js';
|
|
4
|
+
import { logHttp, logMe } from './logger/index.js';
|
|
5
|
+
import { CookiesManager } from './middlewares/cookies/cookie-manager.js';
|
|
6
|
+
import { types } from 'util';
|
|
7
|
+
import { Writable } from 'stream';
|
|
8
|
+
import url from 'url';
|
|
9
|
+
const generateId = hyperid();
|
|
10
|
+
const statusTuples = Object.entries(StatusCodes).reduce((acc, [name, status]) => {
|
|
11
|
+
acc[status] = StatusPhrases[name];
|
|
12
|
+
return acc;
|
|
13
|
+
}, {});
|
|
14
|
+
export class BareRequest {
|
|
15
|
+
_originalRequest;
|
|
16
|
+
_originalResponse;
|
|
17
|
+
ID;
|
|
18
|
+
params;
|
|
19
|
+
query = {};
|
|
20
|
+
remoteIp;
|
|
21
|
+
requestBody;
|
|
22
|
+
requestHeaders;
|
|
23
|
+
statusToSend = 200;
|
|
24
|
+
cm;
|
|
25
|
+
sent = false;
|
|
26
|
+
cache = true;
|
|
27
|
+
startTime;
|
|
28
|
+
startDate = new Date();
|
|
29
|
+
remoteClient = '';
|
|
30
|
+
logging = false;
|
|
31
|
+
requestTimeFormat;
|
|
32
|
+
headers = {};
|
|
33
|
+
cookies = {};
|
|
34
|
+
contentType;
|
|
35
|
+
timeout;
|
|
36
|
+
constructor(_originalRequest, _originalResponse, options) {
|
|
37
|
+
this._originalRequest = _originalRequest;
|
|
38
|
+
this._originalResponse = _originalResponse;
|
|
39
|
+
this.params = {};
|
|
40
|
+
this.ID = { code: _originalRequest.headers['x-request-id'] || generateId() };
|
|
41
|
+
this.remoteIp = _originalRequest.socket.remoteAddress;
|
|
42
|
+
this.contentType = this._originalRequest.headers['content-type'];
|
|
43
|
+
this.requestHeaders = this._originalRequest.headers;
|
|
44
|
+
this.logging = options?.logging ?? false;
|
|
45
|
+
// this is a placeholder URL base that we need to make class working
|
|
46
|
+
new url.URL(`http://localhost/${this._originalRequest.url}`).searchParams.forEach((value, name) => (this.query[name] = value));
|
|
47
|
+
// parsed;
|
|
48
|
+
_originalRequest['flow'] = this; // to receive flow object later on in the route handler
|
|
49
|
+
this.addHeaders({
|
|
50
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
51
|
+
'X-Request-Id': this.ID.code,
|
|
52
|
+
});
|
|
53
|
+
if (options?.requestTimeFormat) {
|
|
54
|
+
this.startTime = process.hrtime();
|
|
55
|
+
this.requestTimeFormat = options.requestTimeFormat;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
readBody() {
|
|
59
|
+
switch (this._originalRequest.method) {
|
|
60
|
+
case 'POST':
|
|
61
|
+
case 'PATCH':
|
|
62
|
+
case 'PUT':
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const temp = [];
|
|
65
|
+
this._originalRequest
|
|
66
|
+
.on('data', (chunk) => temp.push(chunk))
|
|
67
|
+
.on('end', () => {
|
|
68
|
+
const parsed = this.classifyRequestBody(temp);
|
|
69
|
+
if (types.isNativeError(parsed))
|
|
70
|
+
return reject(parsed);
|
|
71
|
+
this.requestBody = parsed;
|
|
72
|
+
resolve(parsed);
|
|
73
|
+
})
|
|
74
|
+
.on('error', reject);
|
|
75
|
+
});
|
|
76
|
+
default:
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
attachCookieManager(opts) {
|
|
81
|
+
this.cm = new CookiesManager(opts, this);
|
|
82
|
+
}
|
|
83
|
+
populateCookies() {
|
|
84
|
+
this.cookies = this.cm?.parseCookie(this._originalRequest.headers.cookie) || {};
|
|
85
|
+
}
|
|
86
|
+
classifyRequestBody(data) {
|
|
87
|
+
const wholeChunk = Buffer.concat(data);
|
|
88
|
+
switch (this.contentType) {
|
|
89
|
+
case 'text/plain':
|
|
90
|
+
return wholeChunk.toString();
|
|
91
|
+
case 'application/json':
|
|
92
|
+
return JSONParse(wholeChunk.toString());
|
|
93
|
+
case 'application/x-www-form-urlencoded':
|
|
94
|
+
const store = {};
|
|
95
|
+
for (const curr of wholeChunk.toString().split('&')) {
|
|
96
|
+
const [key, value] = curr.split('=');
|
|
97
|
+
if (!key || !value)
|
|
98
|
+
return null; // form urlencoded is not correct
|
|
99
|
+
store[key] = value;
|
|
100
|
+
}
|
|
101
|
+
return store;
|
|
102
|
+
default:
|
|
103
|
+
return wholeChunk;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
setRemoteClient(remoteClient) {
|
|
107
|
+
this.remoteClient = remoteClient;
|
|
108
|
+
}
|
|
109
|
+
setRequestTime() {
|
|
110
|
+
const diff = process.hrtime(this.startTime);
|
|
111
|
+
const time = diff[0] * (this.requestTimeFormat === 's' ? 1 : 1e3) +
|
|
112
|
+
diff[1] * (this.requestTimeFormat === 's' ? 1e-9 : 1e-6);
|
|
113
|
+
this.setHeaders({
|
|
114
|
+
'X-Processing-Time': time,
|
|
115
|
+
'X-Processing-Time-Mode': this.requestTimeFormat === 's' ? 'seconds' : 'milliseconds',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
cleanHeader(header) {
|
|
119
|
+
delete this.headers[header];
|
|
120
|
+
}
|
|
121
|
+
attachTimeout(timeout) {
|
|
122
|
+
if (this.timeout)
|
|
123
|
+
clearTimeout(this.timeout);
|
|
124
|
+
this.timeout = setTimeout(() => {
|
|
125
|
+
this.status(503)._send('Server aborted connection by overtime');
|
|
126
|
+
}, timeout);
|
|
127
|
+
// attach listener to clear the timeout
|
|
128
|
+
this._originalResponse.on('close', () => this.timeout && clearTimeout(this.timeout));
|
|
129
|
+
}
|
|
130
|
+
setParams(params) {
|
|
131
|
+
this.params = params;
|
|
132
|
+
}
|
|
133
|
+
// ======== PUBLIC APIS ========
|
|
134
|
+
getHeader(header) {
|
|
135
|
+
return this.headers[header];
|
|
136
|
+
}
|
|
137
|
+
getCookie(cookie) {
|
|
138
|
+
return this.cookies[cookie];
|
|
139
|
+
}
|
|
140
|
+
getCookies() {
|
|
141
|
+
return { ...this.cookies };
|
|
142
|
+
}
|
|
143
|
+
disableCache() {
|
|
144
|
+
this.cache = false;
|
|
145
|
+
}
|
|
146
|
+
setCache(cacheOpts) {
|
|
147
|
+
const cacheHeader = [];
|
|
148
|
+
const directive = 'Cache-Control';
|
|
149
|
+
if (cacheOpts.cacheability)
|
|
150
|
+
cacheHeader.push(cacheOpts.cacheability);
|
|
151
|
+
if (cacheOpts.expirationKind)
|
|
152
|
+
cacheHeader.push(`${cacheOpts.expirationKind}=${cacheOpts.expirationSeconds ?? 3600}`);
|
|
153
|
+
if (cacheOpts.revalidation)
|
|
154
|
+
cacheHeader.push(cacheOpts.revalidation);
|
|
155
|
+
if (cacheHeader.length > 0)
|
|
156
|
+
this.setHeader(directive, cacheHeader);
|
|
157
|
+
}
|
|
158
|
+
addHeader(header, value) {
|
|
159
|
+
const old = this.headers[header];
|
|
160
|
+
const parsedVal = Array.isArray(value) ? value.join(', ') : '' + value;
|
|
161
|
+
if (old) {
|
|
162
|
+
this.headers[header] += `, ${parsedVal}`;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
this.headers[header] = parsedVal;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
setHeader(header, value) {
|
|
169
|
+
const parsedVal = Array.isArray(value) ? value.join(', ') : '' + value;
|
|
170
|
+
this.headers[header] = parsedVal;
|
|
171
|
+
}
|
|
172
|
+
setHeaders(headers) {
|
|
173
|
+
for (const [header, value] of Object.entries(headers)) {
|
|
174
|
+
this.setHeader(header, value);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
addHeaders(headers) {
|
|
178
|
+
for (const [header, value] of Object.entries(headers)) {
|
|
179
|
+
this.addHeader(header, value);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
status(status) {
|
|
183
|
+
this.statusToSend = status;
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
sendStatus(status) {
|
|
187
|
+
this.status(status)._send();
|
|
188
|
+
}
|
|
189
|
+
stream(stream) {
|
|
190
|
+
this._originalResponse.pipe(stream, { end: true });
|
|
191
|
+
}
|
|
192
|
+
json(data) {
|
|
193
|
+
// to generate with fast-json-stringify schema issue #1
|
|
194
|
+
const jsoned = JSONStringify(data);
|
|
195
|
+
this.setHeader('Content-Type', 'application/json');
|
|
196
|
+
this._send(jsoned ? jsoned : undefined);
|
|
197
|
+
}
|
|
198
|
+
_send(chunk) {
|
|
199
|
+
if (this._originalResponse.socket?.destroyed) {
|
|
200
|
+
logMe.error("Tying to send into closed client's stream");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (this._originalResponse.headersSent || this.sent) {
|
|
204
|
+
logMe.error('Trying to send with the headers already sent');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// work basic headers
|
|
208
|
+
if (typeof chunk !== 'undefined' && chunk !== null)
|
|
209
|
+
this.setHeader('Content-Length', Buffer.byteLength(chunk, 'utf-8'));
|
|
210
|
+
if (!this.cache)
|
|
211
|
+
this.setHeaders({ 'Cache-Control': 'no-store', Expire: 0, Pragma: 'no-cache' });
|
|
212
|
+
if (this.statusToSend >= 400 && this.statusToSend !== 404 && this.statusToSend !== 410)
|
|
213
|
+
this.cleanHeader('Cache-Control');
|
|
214
|
+
if (this.requestTimeFormat)
|
|
215
|
+
this.setRequestTime();
|
|
216
|
+
// perform sending
|
|
217
|
+
this._originalResponse.writeHead(this.statusToSend, '', this.headers);
|
|
218
|
+
this._originalResponse.end(chunk || statusTuples[this.statusToSend]);
|
|
219
|
+
this.sent = true;
|
|
220
|
+
// call logging section
|
|
221
|
+
if (this.logging === true) {
|
|
222
|
+
logHttp(this.headers, this.startDate, this.remoteClient, this._originalRequest, this._originalResponse);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
sendStringifiedJson(data) {
|
|
226
|
+
this.setHeader('Content-Type', 'application/json');
|
|
227
|
+
this._send(data);
|
|
228
|
+
}
|
|
229
|
+
send(anything) {
|
|
230
|
+
if (this.sent)
|
|
231
|
+
return;
|
|
232
|
+
if (typeof anything === 'undefined' || anything === null)
|
|
233
|
+
return this._send();
|
|
234
|
+
switch (anything.constructor) {
|
|
235
|
+
case Uint8Array:
|
|
236
|
+
case Uint16Array:
|
|
237
|
+
case Uint32Array:
|
|
238
|
+
this._send(Buffer.from(anything.buffer));
|
|
239
|
+
break;
|
|
240
|
+
case Buffer:
|
|
241
|
+
case String:
|
|
242
|
+
this._send(anything);
|
|
243
|
+
break;
|
|
244
|
+
case Boolean:
|
|
245
|
+
case Number:
|
|
246
|
+
this._send('' + anything);
|
|
247
|
+
break;
|
|
248
|
+
case Writable:
|
|
249
|
+
this.stream(anything);
|
|
250
|
+
break;
|
|
251
|
+
case Object:
|
|
252
|
+
case Array:
|
|
253
|
+
this.json(anything);
|
|
254
|
+
break;
|
|
255
|
+
default:
|
|
256
|
+
this._send();
|
|
257
|
+
logMe.warn('Unknown type to send');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ts, Type } from 'ts-morph';
|
|
2
|
+
export type StringSchemaType = {
|
|
3
|
+
type: 'string';
|
|
4
|
+
nullable: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type NumberSchemaType = {
|
|
7
|
+
type: 'number';
|
|
8
|
+
nullable: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type BooleanSchemaType = {
|
|
11
|
+
type: 'boolean';
|
|
12
|
+
nullable: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type ArraySchemaType = {
|
|
15
|
+
type: 'array';
|
|
16
|
+
items: StringSchemaType | NumberSchemaType | BooleanSchemaType | ArraySchemaType | ObjectSchemaType;
|
|
17
|
+
nullable: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type ObjectSchemaType = {
|
|
20
|
+
type: 'object';
|
|
21
|
+
properties: {
|
|
22
|
+
[key: string]: StringSchemaType | NumberSchemaType | BooleanSchemaType | ArraySchemaType | ObjectSchemaType;
|
|
23
|
+
};
|
|
24
|
+
nullable: boolean;
|
|
25
|
+
};
|
|
26
|
+
export type UnionSchemaType = {
|
|
27
|
+
type: 'union';
|
|
28
|
+
anyOf: CustomSchema[];
|
|
29
|
+
nullable: boolean;
|
|
30
|
+
};
|
|
31
|
+
export type CustomSchema = StringSchemaType | NumberSchemaType | BooleanSchemaType | ArraySchemaType | ObjectSchemaType | UnionSchemaType;
|
|
32
|
+
export declare const generateCustomSchema: (t: Type<ts.Type>) => CustomSchema;
|