@trayio/express 4.92.0 → 4.94.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/dist/http/ExpressHttpController.d.ts.map +1 -1
- package/dist/http/ExpressHttpController.js +12 -1
- package/dist/http/ExpressHttpController.unit.test.d.ts +2 -0
- package/dist/http/ExpressHttpController.unit.test.d.ts.map +1 -0
- package/dist/http/ExpressHttpController.unit.test.js +288 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpressHttpController.d.ts","sourceRoot":"","sources":["../../src/http/ExpressHttpController.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AASnD,OAAO,EACN,cAAc,EAEd,MAAM,qCAAqC,CAAC;AAG7C,OAAO,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAW1D,qBAAa,qBAAqB;IAEhC,SAAS,CAAC,UAAU,EAAE,cAAc;IACpC,SAAS,CAAC,2BAA2B,EAAE,MAAM;IAC7C,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;gBAFzB,UAAU,EAAE,cAAc,EAC1B,2BAA2B,EAAE,MAAe,EAC5C,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;IAGpC,OAAO,CAAC,QAAQ,
|
|
1
|
+
{"version":3,"file":"ExpressHttpController.d.ts","sourceRoot":"","sources":["../../src/http/ExpressHttpController.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AASnD,OAAO,EACN,cAAc,EAEd,MAAM,qCAAqC,CAAC;AAG7C,OAAO,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAW1D,qBAAa,qBAAqB;IAEhC,SAAS,CAAC,UAAU,EAAE,cAAc;IACpC,SAAS,CAAC,2BAA2B,EAAE,MAAM;IAC7C,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;gBAFzB,UAAU,EAAE,cAAc,EAC1B,2BAA2B,EAAE,MAAe,EAC5C,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;IAGpC,OAAO,CAAC,QAAQ,CA2Fd;IAEF,SAAS,WAAY,MAAM,YAKzB;IAEF,OAAO,CAAC,gBAAgB,CAoBtB;IAGF,OAAO,CAAC,uBAAuB,CAI7B;IAEF,OAAO,CAAC,sBAAsB,CAmC3B;IAEH,OAAO,CAAC,aAAa,CAenB;IAEF,OAAO,CAAC,YAAY,CAkBlB;IAEF,OAAO,CAAC,+BAA+B,CAWpC;CACH"}
|
|
@@ -78,8 +78,19 @@ class ExpressHttpController {
|
|
|
78
78
|
queryString: req.query,
|
|
79
79
|
body: requestBody,
|
|
80
80
|
}))();
|
|
81
|
+
const sanitizeHeaderValue = (value) => {
|
|
82
|
+
let processedValue = value;
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
processedValue = value.join(','); // flatten arrays
|
|
85
|
+
}
|
|
86
|
+
// Convert to string and sanitize for HTTP header compliance
|
|
87
|
+
// Keep only valid HTTP header characters: tab, printable ASCII (0x20-0x7E), and extended ASCII (0x80-0xFF)
|
|
88
|
+
return String(processedValue)
|
|
89
|
+
.replace(/[^\t\x20-\x7e\x80-\xff]/g, '_')
|
|
90
|
+
.trim();
|
|
91
|
+
};
|
|
81
92
|
const response = Object.entries(httpResponse.headers)
|
|
82
|
-
.reduce((acc, [key, value]) => acc.setHeader(key, value), res)
|
|
93
|
+
.reduce((acc, [key, value]) => acc.setHeader(key, sanitizeHeaderValue(value)), res)
|
|
83
94
|
.status(httpResponse.statusCode);
|
|
84
95
|
httpResponse.body.pipe(response);
|
|
85
96
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpressHttpController.unit.test.d.ts","sourceRoot":"","sources":["../../src/http/ExpressHttpController.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
const Http_1 = require("@trayio/commons/http/Http");
|
|
27
|
+
const O = __importStar(require("fp-ts/Option"));
|
|
28
|
+
const T = __importStar(require("fp-ts/Task"));
|
|
29
|
+
const stream_1 = require("stream");
|
|
30
|
+
const ExpressHttpController_1 = require("./ExpressHttpController");
|
|
31
|
+
describe('ExpressHttpController', () => {
|
|
32
|
+
let mockHttpController;
|
|
33
|
+
let mockEndpoint;
|
|
34
|
+
let expressHttpController;
|
|
35
|
+
let mockRouter;
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
mockEndpoint = {
|
|
38
|
+
path: '/test',
|
|
39
|
+
method: Http_1.HttpMethod.Get,
|
|
40
|
+
execute: jest.fn(),
|
|
41
|
+
};
|
|
42
|
+
mockHttpController = {
|
|
43
|
+
getEndpoints: jest.fn().mockReturnValue([mockEndpoint]),
|
|
44
|
+
};
|
|
45
|
+
expressHttpController = new ExpressHttpController_1.ExpressHttpController(mockHttpController, '/tmp', O.none);
|
|
46
|
+
mockRouter = {
|
|
47
|
+
get: jest.fn(),
|
|
48
|
+
post: jest.fn(),
|
|
49
|
+
put: jest.fn(),
|
|
50
|
+
delete: jest.fn(),
|
|
51
|
+
patch: jest.fn(),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
describe('addRoutes', () => {
|
|
55
|
+
it('should add GET route to router', () => {
|
|
56
|
+
expressHttpController.addRoutes(mockRouter);
|
|
57
|
+
expect(mockRouter.get).toHaveBeenCalledWith('/test', expect.any(Function));
|
|
58
|
+
});
|
|
59
|
+
it('should add POST route to router', () => {
|
|
60
|
+
mockEndpoint.method = Http_1.HttpMethod.Post;
|
|
61
|
+
expressHttpController.addRoutes(mockRouter);
|
|
62
|
+
expect(mockRouter.post).toHaveBeenCalledWith('/test', expect.any(Function));
|
|
63
|
+
});
|
|
64
|
+
it('should add PUT route to router', () => {
|
|
65
|
+
mockEndpoint.method = Http_1.HttpMethod.Put;
|
|
66
|
+
expressHttpController.addRoutes(mockRouter);
|
|
67
|
+
expect(mockRouter.put).toHaveBeenCalledWith('/test', expect.any(Function));
|
|
68
|
+
});
|
|
69
|
+
it('should add DELETE route to router', () => {
|
|
70
|
+
mockEndpoint.method = Http_1.HttpMethod.Delete;
|
|
71
|
+
expressHttpController.addRoutes(mockRouter);
|
|
72
|
+
expect(mockRouter.delete).toHaveBeenCalledWith('/test', expect.any(Function));
|
|
73
|
+
});
|
|
74
|
+
it('should add PATCH route to router', () => {
|
|
75
|
+
mockEndpoint.method = Http_1.HttpMethod.Patch;
|
|
76
|
+
expressHttpController.addRoutes(mockRouter);
|
|
77
|
+
expect(mockRouter.patch).toHaveBeenCalledWith('/test', expect.any(Function));
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('header sanitization', () => {
|
|
81
|
+
let mockReq;
|
|
82
|
+
let mockRes;
|
|
83
|
+
let routeHandler;
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
mockReq = {
|
|
86
|
+
headers: {},
|
|
87
|
+
params: {},
|
|
88
|
+
query: {},
|
|
89
|
+
body: {},
|
|
90
|
+
path: '/test',
|
|
91
|
+
};
|
|
92
|
+
mockRes = {
|
|
93
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
94
|
+
status: jest.fn().mockReturnThis(),
|
|
95
|
+
on: jest.fn(),
|
|
96
|
+
once: jest.fn(),
|
|
97
|
+
end: jest.fn(),
|
|
98
|
+
write: jest.fn(),
|
|
99
|
+
removeListener: jest.fn(),
|
|
100
|
+
emit: jest.fn(),
|
|
101
|
+
};
|
|
102
|
+
const mockResponse = new stream_1.Readable();
|
|
103
|
+
mockResponse.push(null);
|
|
104
|
+
const httpResponse = {
|
|
105
|
+
statusCode: 200,
|
|
106
|
+
headers: {},
|
|
107
|
+
body: mockResponse,
|
|
108
|
+
};
|
|
109
|
+
mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
|
|
110
|
+
expressHttpController.addRoutes(mockRouter);
|
|
111
|
+
// eslint-disable-next-line prefer-destructuring
|
|
112
|
+
routeHandler = mockRouter.get.mock.calls[0][1];
|
|
113
|
+
});
|
|
114
|
+
it('should sanitize header values with control characters', async () => {
|
|
115
|
+
const httpResponse = {
|
|
116
|
+
statusCode: 200,
|
|
117
|
+
headers: {
|
|
118
|
+
'test-header': 'value\x00with\x1fcontrol\x7fchars',
|
|
119
|
+
},
|
|
120
|
+
body: new stream_1.Readable({
|
|
121
|
+
read() {
|
|
122
|
+
this.push(null);
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
};
|
|
126
|
+
mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
|
|
127
|
+
await routeHandler(mockReq, mockRes);
|
|
128
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('test-header', 'value_with_control_chars');
|
|
129
|
+
});
|
|
130
|
+
it('should sanitize header values with line breaks', async () => {
|
|
131
|
+
const httpResponse = {
|
|
132
|
+
statusCode: 200,
|
|
133
|
+
headers: {
|
|
134
|
+
'test-header': 'value\nwith\rline\r\nbreaks',
|
|
135
|
+
},
|
|
136
|
+
body: new stream_1.Readable({
|
|
137
|
+
read() {
|
|
138
|
+
this.push(null);
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
|
|
143
|
+
await routeHandler(mockReq, mockRes);
|
|
144
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('test-header', 'value_with_line__breaks');
|
|
145
|
+
});
|
|
146
|
+
it('should preserve quotes and backslashes as valid characters', async () => {
|
|
147
|
+
const httpResponse = {
|
|
148
|
+
statusCode: 200,
|
|
149
|
+
headers: {
|
|
150
|
+
'test-header': 'value"with\\quotes',
|
|
151
|
+
},
|
|
152
|
+
body: new stream_1.Readable({
|
|
153
|
+
read() {
|
|
154
|
+
this.push(null);
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
|
|
159
|
+
await routeHandler(mockReq, mockRes);
|
|
160
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('test-header', 'value"with\\quotes');
|
|
161
|
+
});
|
|
162
|
+
it('should trim whitespace from header values', async () => {
|
|
163
|
+
const httpResponse = {
|
|
164
|
+
statusCode: 200,
|
|
165
|
+
headers: {
|
|
166
|
+
'test-header': ' value with spaces ',
|
|
167
|
+
},
|
|
168
|
+
body: new stream_1.Readable({
|
|
169
|
+
read() {
|
|
170
|
+
this.push(null);
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
};
|
|
174
|
+
mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
|
|
175
|
+
await routeHandler(mockReq, mockRes);
|
|
176
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('test-header', 'value with spaces');
|
|
177
|
+
});
|
|
178
|
+
it('should handle array header values by joining with comma', async () => {
|
|
179
|
+
const httpResponse = {
|
|
180
|
+
statusCode: 200,
|
|
181
|
+
headers: {
|
|
182
|
+
'test-header': ['value1', 'value2', 'value3'],
|
|
183
|
+
},
|
|
184
|
+
body: new stream_1.Readable({
|
|
185
|
+
read() {
|
|
186
|
+
this.push(null);
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
|
|
191
|
+
await routeHandler(mockReq, mockRes);
|
|
192
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('test-header', 'value1,value2,value3');
|
|
193
|
+
});
|
|
194
|
+
it('should handle numeric header values', async () => {
|
|
195
|
+
const httpResponse = {
|
|
196
|
+
statusCode: 200,
|
|
197
|
+
headers: {
|
|
198
|
+
'content-length': '123',
|
|
199
|
+
},
|
|
200
|
+
body: new stream_1.Readable({
|
|
201
|
+
read() {
|
|
202
|
+
this.push(null);
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
|
|
207
|
+
await routeHandler(mockReq, mockRes);
|
|
208
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('content-length', '123');
|
|
209
|
+
});
|
|
210
|
+
it('should sanitize complex header values with multiple issues', async () => {
|
|
211
|
+
const httpResponse = {
|
|
212
|
+
statusCode: 200,
|
|
213
|
+
headers: {
|
|
214
|
+
'test-header': ' value\x00with\r\n"multiple\\issues" ',
|
|
215
|
+
},
|
|
216
|
+
body: new stream_1.Readable({
|
|
217
|
+
read() {
|
|
218
|
+
this.push(null);
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
};
|
|
222
|
+
mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
|
|
223
|
+
await routeHandler(mockReq, mockRes);
|
|
224
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('test-header', 'value_with__"multiple\\issues"');
|
|
225
|
+
});
|
|
226
|
+
it('should sanitize header values with em dash character', async () => {
|
|
227
|
+
const httpResponse = {
|
|
228
|
+
statusCode: 200,
|
|
229
|
+
headers: {
|
|
230
|
+
'test-header': 'value–with–em–dash',
|
|
231
|
+
},
|
|
232
|
+
body: new stream_1.Readable({
|
|
233
|
+
read() {
|
|
234
|
+
this.push(null);
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
};
|
|
238
|
+
mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
|
|
239
|
+
await routeHandler(mockReq, mockRes);
|
|
240
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('test-header', 'value_with_em_dash');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
describe('error handling', () => {
|
|
244
|
+
let mockReq;
|
|
245
|
+
let mockRes;
|
|
246
|
+
let routeHandler;
|
|
247
|
+
beforeEach(() => {
|
|
248
|
+
mockReq = {
|
|
249
|
+
headers: { 'content-type': 'multipart/form-data' },
|
|
250
|
+
params: {},
|
|
251
|
+
query: {},
|
|
252
|
+
body: {},
|
|
253
|
+
path: '/test',
|
|
254
|
+
};
|
|
255
|
+
mockRes = {
|
|
256
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
257
|
+
status: jest.fn().mockReturnThis(),
|
|
258
|
+
json: jest.fn().mockReturnThis(),
|
|
259
|
+
};
|
|
260
|
+
expressHttpController = new ExpressHttpController_1.ExpressHttpController(mockHttpController, '/tmp', O.some({
|
|
261
|
+
warning: jest.fn(),
|
|
262
|
+
}));
|
|
263
|
+
expressHttpController.addRoutes(mockRouter);
|
|
264
|
+
// eslint-disable-next-line prefer-destructuring
|
|
265
|
+
routeHandler = mockRouter.get.mock.calls[0][1];
|
|
266
|
+
});
|
|
267
|
+
it('should handle file too large error', async () => {
|
|
268
|
+
jest
|
|
269
|
+
.spyOn(expressHttpController, 'parseRequestBody')
|
|
270
|
+
.mockRejectedValue(new Error('maxFileSize exceeded'));
|
|
271
|
+
await routeHandler(mockReq, mockRes);
|
|
272
|
+
expect(mockRes.status).toHaveBeenCalledWith(400);
|
|
273
|
+
expect(mockRes.json).toHaveBeenCalledWith({
|
|
274
|
+
error: 'File too large. Max size is 100MB.',
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
it('should handle general parsing errors', async () => {
|
|
278
|
+
jest
|
|
279
|
+
.spyOn(expressHttpController, 'parseRequestBody')
|
|
280
|
+
.mockRejectedValue(new Error('Invalid format'));
|
|
281
|
+
await routeHandler(mockReq, mockRes);
|
|
282
|
+
expect(mockRes.status).toHaveBeenCalledWith(400);
|
|
283
|
+
expect(mockRes.json).toHaveBeenCalledWith({
|
|
284
|
+
error: 'Invalid request: Invalid format',
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trayio/express",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.94.0",
|
|
4
4
|
"description": "Express extensions and implementations",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./*": "./dist/*.js"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@trayio/commons": "4.
|
|
17
|
+
"@trayio/commons": "4.94.0",
|
|
18
18
|
"cors": "2.8.5",
|
|
19
19
|
"express": "4.20.0",
|
|
20
20
|
"formidable": "3.5.1"
|