@travetto/web 6.0.1 → 6.0.2
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 +8 -15
- package/__index__.ts +2 -1
- package/package.json +2 -8
- package/src/interceptor/accept.ts +1 -2
- package/src/interceptor/body.ts +5 -3
- package/src/interceptor/cache-control.ts +8 -16
- package/src/interceptor/compress.ts +8 -16
- package/src/interceptor/cookie.ts +1 -1
- package/src/interceptor/etag.ts +2 -12
- package/src/types/cookie.ts +1 -1
- package/src/types/headers.ts +1 -42
- package/src/util/body.ts +0 -15
- package/src/util/common.ts +20 -1
- package/src/util/cookie.ts +8 -49
- package/src/util/header.ts +161 -0
- package/src/util/keygrip.ts +43 -0
- package/support/test/dispatch-util.ts +7 -4
- package/support/test/suite/base.ts +3 -0
- package/src/util/mime.ts +0 -36
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ export class WebResponse<B = unknown> extends BaseWebMessage<B, WebResponseConte
|
|
|
88
88
|
}
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
These objects do not represent the underlying sockets provided by various http servers, but in fact are simple wrappers that track the flow through the call stack of the various [WebInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/types/interceptor.ts#L15)s and the [Endpoint](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L14) handler. One of the biggest departures here, is that the response is not an entity that is passed around from call-site to call-site, but is is solely a return-value. This doesn't mean the return value has to be static and pre-allocated, on the contrary streams are still supported. The difference here is that the streams/asynchronous values will be consumed until the response is sent back to the user. The [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#
|
|
91
|
+
These objects do not represent the underlying sockets provided by various http servers, but in fact are simple wrappers that track the flow through the call stack of the various [WebInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/types/interceptor.ts#L15)s and the [Endpoint](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L14) handler. One of the biggest departures here, is that the response is not an entity that is passed around from call-site to call-site, but is is solely a return-value. This doesn't mean the return value has to be static and pre-allocated, on the contrary streams are still supported. The difference here is that the streams/asynchronous values will be consumed until the response is sent back to the user. The [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L44) is a good reference for transforming a [WebResponse](https://github.com/travetto/travetto/tree/main/module/web/src/types/response.ts#L3) that can either be a stream or a fixed value.
|
|
92
92
|
|
|
93
93
|
## Defining a Controller
|
|
94
94
|
To start, we must define a [@Controller](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/controller.ts#L9), which is only allowed on classes. Controllers can be configured with:
|
|
@@ -378,8 +378,8 @@ Out of the box, the web framework comes with a few interceptors, and more are co
|
|
|
378
378
|
1. global - Intended to run outside of the request flow - [AsyncContextInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/context.ts#L13)
|
|
379
379
|
1. terminal - Handles once request and response are finished building - [LoggingInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/logging.ts#L28), [RespondInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/respond.ts#L12)
|
|
380
380
|
1. pre-request - Prepares the request for running - [TrustProxyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/trust-proxy.ts#L23)
|
|
381
|
-
1. request - Handles inbound request, validation, and body preparation - [DecompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/decompress.ts#L53), [AcceptInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/accept.ts#
|
|
382
|
-
1. response - Prepares outbound response - [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#
|
|
381
|
+
1. request - Handles inbound request, validation, and body preparation - [DecompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/decompress.ts#L53), [AcceptInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/accept.ts#L33), [BodyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/body.ts#L58), [CookieInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cookie.ts#L60)
|
|
382
|
+
1. response - Prepares outbound response - [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L44), [CorsInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cors.ts#L51), [EtagInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/etag.ts#L43), [CacheControlInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cache-control.ts#L23)
|
|
383
383
|
1. application - Lives outside of the general request/response behavior, [Web Auth](https://github.com/travetto/travetto/tree/main/module/auth-web#readme "Web authentication integration support for the Travetto framework") uses this for login and logout flows.
|
|
384
384
|
|
|
385
385
|
### Packaged Interceptors
|
|
@@ -425,7 +425,7 @@ export class TrustProxyConfig {
|
|
|
425
425
|
```
|
|
426
426
|
|
|
427
427
|
#### AcceptInterceptor
|
|
428
|
-
[AcceptInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/accept.ts#
|
|
428
|
+
[AcceptInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/accept.ts#L33) handles verifying the inbound request matches the allowed content-types. This acts as a standard gate-keeper for spurious input.
|
|
429
429
|
|
|
430
430
|
**Code: Accept Config**
|
|
431
431
|
```typescript
|
|
@@ -478,7 +478,7 @@ export class CookieConfig implements CookieSetOptions {
|
|
|
478
478
|
/**
|
|
479
479
|
* Supported only via http (not in JS)
|
|
480
480
|
*/
|
|
481
|
-
|
|
481
|
+
httponly = true;
|
|
482
482
|
/**
|
|
483
483
|
* Enforce same site policy
|
|
484
484
|
*/
|
|
@@ -504,7 +504,7 @@ export class CookieConfig implements CookieSetOptions {
|
|
|
504
504
|
```
|
|
505
505
|
|
|
506
506
|
#### BodyInterceptor
|
|
507
|
-
[BodyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/body.ts#
|
|
507
|
+
[BodyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/body.ts#L58) handles the inbound request, and converting the body payload into an appropriate format.
|
|
508
508
|
|
|
509
509
|
**Code: Body Config**
|
|
510
510
|
```typescript
|
|
@@ -532,7 +532,7 @@ export class WebBodyConfig {
|
|
|
532
532
|
```
|
|
533
533
|
|
|
534
534
|
#### CompressInterceptor
|
|
535
|
-
[CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#
|
|
535
|
+
[CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L44) by default, will compress all valid outbound responses over a certain size, or for streams will cache every response. This relies on Node's [Buffer](https://nodejs.org/api/zlib.html) support for compression.
|
|
536
536
|
|
|
537
537
|
**Code: Compress Config**
|
|
538
538
|
```typescript
|
|
@@ -545,10 +545,6 @@ export class CompressConfig {
|
|
|
545
545
|
* Raw encoding options
|
|
546
546
|
*/
|
|
547
547
|
raw?: (ZlibOptions & BrotliOptions) | undefined;
|
|
548
|
-
/**
|
|
549
|
-
* Preferred encodings
|
|
550
|
-
*/
|
|
551
|
-
preferredEncodings?: WebCompressEncoding[] = ['br', 'gzip', 'identity'];
|
|
552
548
|
/**
|
|
553
549
|
* Supported encodings
|
|
554
550
|
*/
|
|
@@ -739,14 +735,11 @@ export class SimpleAuthInterceptor implements WebInterceptor {
|
|
|
739
735
|
```
|
|
740
736
|
|
|
741
737
|
## Cookie Support
|
|
742
|
-
Cookies are a unique element, within the framework, as they sit on the request and response flows. Ideally we would separate these out, but given the support for key rotation, there is a scenario in which reading a cookie on the request, will result in a cookie needing to be written on the response. Because of this, cookies are treated as being outside the normal [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11) activity, and is exposed as the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L61) [CookieJar](https://github.com/travetto/travetto/tree/main/module/web/src/util/cookie.ts#
|
|
738
|
+
Cookies are a unique element, within the framework, as they sit on the request and response flows. Ideally we would separate these out, but given the support for key rotation, there is a scenario in which reading a cookie on the request, will result in a cookie needing to be written on the response. Because of this, cookies are treated as being outside the normal [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11) activity, and is exposed as the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L61) [CookieJar](https://github.com/travetto/travetto/tree/main/module/web/src/util/cookie.ts#L12). The [CookieJar](https://github.com/travetto/travetto/tree/main/module/web/src/util/cookie.ts#L12) has a fairly basic contract:
|
|
743
739
|
|
|
744
740
|
**Code: CookieJar contract**
|
|
745
741
|
```typescript
|
|
746
742
|
export class CookieJar {
|
|
747
|
-
static parseCookieHeader(header: string): Cookie[];
|
|
748
|
-
static parseSetCookieHeader(header: string): Cookie;
|
|
749
|
-
static responseSuffix(c: Cookie): string[];
|
|
750
743
|
constructor({ keys, ...options }: CookieJarOptions = {});
|
|
751
744
|
import(cookies: Cookie[]): this;
|
|
752
745
|
has(name: string, opts: CookieGetOptions = {}): boolean;
|
package/__index__.ts
CHANGED
|
@@ -38,7 +38,8 @@ export * from './src/interceptor/trust-proxy.ts';
|
|
|
38
38
|
|
|
39
39
|
export * from './src/util/body.ts';
|
|
40
40
|
export * from './src/util/endpoint.ts';
|
|
41
|
-
export * from './src/util/
|
|
41
|
+
export * from './src/util/header.ts';
|
|
42
42
|
export * from './src/util/cookie.ts';
|
|
43
43
|
export * from './src/util/common.ts';
|
|
44
|
+
export * from './src/util/keygrip.ts';
|
|
44
45
|
export * from './src/util/net.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/web",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.2",
|
|
4
4
|
"description": "Declarative support creating for Web Applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web",
|
|
@@ -31,13 +31,7 @@
|
|
|
31
31
|
"@travetto/registry": "^6.0.0",
|
|
32
32
|
"@travetto/runtime": "^6.0.0",
|
|
33
33
|
"@travetto/schema": "^6.0.0",
|
|
34
|
-
"
|
|
35
|
-
"@types/keygrip": "^1.0.6",
|
|
36
|
-
"@types/negotiator": "^0.6.3",
|
|
37
|
-
"find-my-way": "^9.3.0",
|
|
38
|
-
"fresh": "^0.5.2",
|
|
39
|
-
"keygrip": "^1.1.0",
|
|
40
|
-
"negotiator": "^1.0.0"
|
|
34
|
+
"find-my-way": "^9.3.0"
|
|
41
35
|
},
|
|
42
36
|
"peerDependencies": {
|
|
43
37
|
"@travetto/cli": "^6.0.0",
|
|
@@ -2,7 +2,6 @@ import { Injectable, Inject } from '@travetto/di';
|
|
|
2
2
|
import { Config } from '@travetto/config';
|
|
3
3
|
import { Ignore } from '@travetto/schema';
|
|
4
4
|
|
|
5
|
-
import { MimeUtil } from '../util/mime.ts';
|
|
6
5
|
import { WebCommonUtil } from '../util/common.ts';
|
|
7
6
|
|
|
8
7
|
import { WebChainedContext } from '../types/filter.ts';
|
|
@@ -39,7 +38,7 @@ export class AcceptInterceptor implements WebInterceptor<AcceptConfig> {
|
|
|
39
38
|
config: AcceptConfig;
|
|
40
39
|
|
|
41
40
|
finalizeConfig({ config }: WebInterceptorContext<AcceptConfig>): AcceptConfig {
|
|
42
|
-
config.matcher =
|
|
41
|
+
config.matcher = WebCommonUtil.mimeTypeMatcher(config.types ?? []);
|
|
43
42
|
return config;
|
|
44
43
|
}
|
|
45
44
|
|
package/src/interceptor/body.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { ByteSizeInput, WebCommonUtil } from '../util/common.ts';
|
|
|
14
14
|
import { AcceptInterceptor } from './accept.ts';
|
|
15
15
|
import { DecompressInterceptor } from './decompress.ts';
|
|
16
16
|
import { WebError } from '../types/error.ts';
|
|
17
|
+
import { WebHeaderUtil } from '../util/header.ts';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* @concrete
|
|
@@ -90,12 +91,13 @@ export class BodyInterceptor implements WebInterceptor<WebBodyConfig> {
|
|
|
90
91
|
throw WebError.for('Request entity too large', 413, { length, limit });
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
const contentType = request.headers.
|
|
94
|
-
if (!contentType) {
|
|
94
|
+
const contentType = WebHeaderUtil.parseHeaderSegment(request.headers.get('Content-Type'));
|
|
95
|
+
if (!contentType.value) {
|
|
95
96
|
return next();
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
const
|
|
99
|
+
const [baseType,] = contentType.value.split('/');
|
|
100
|
+
const parserType = config.parsingTypes[contentType.value] ?? config.parsingTypes[baseType];
|
|
99
101
|
if (!parserType) {
|
|
100
102
|
return next();
|
|
101
103
|
}
|
|
@@ -35,23 +35,15 @@ export class CacheControlInterceptor implements WebInterceptor {
|
|
|
35
35
|
async filter({ next }: WebChainedContext<CacheControlConfig>): Promise<WebResponse> {
|
|
36
36
|
const response = await next();
|
|
37
37
|
if (!response.headers.has('Cache-Control')) {
|
|
38
|
-
const parts: string[] = [];
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
['no-store', 'max-age=0'] :
|
|
46
|
-
[`max-age=${response.context.cacheableAge}`]
|
|
47
|
-
)
|
|
48
|
-
);
|
|
49
|
-
} else if (response.context.isPrivate) { // If private, but no age, don't store
|
|
50
|
-
parts.push('no-store');
|
|
51
|
-
}
|
|
52
|
-
if (parts.length) {
|
|
53
|
-
response.headers.set('Cache-Control', parts.join(','));
|
|
38
|
+
const parts: string[] = [response.context.isPrivate ? 'private' : 'public'];
|
|
39
|
+
const age = response.context.cacheableAge ?? (response.context.isPrivate ? 0 : undefined);
|
|
40
|
+
if (age !== undefined) {
|
|
41
|
+
if (age <= 0) {
|
|
42
|
+
parts.push('no-store');
|
|
43
|
+
}
|
|
44
|
+
parts.push(`max-age=${Math.max(age, 0)}`);
|
|
54
45
|
}
|
|
46
|
+
response.headers.set('Cache-Control', parts.join(','));
|
|
55
47
|
}
|
|
56
48
|
return response;
|
|
57
49
|
}
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { buffer } from 'node:stream/consumers';
|
|
2
2
|
import { BrotliOptions, constants, createBrotliCompress, createDeflate, createGzip, ZlibOptions } from 'node:zlib';
|
|
3
3
|
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
5
|
-
import Negotiator from 'negotiator';
|
|
6
|
-
|
|
7
4
|
import { Injectable, Inject } from '@travetto/di';
|
|
8
5
|
import { Config } from '@travetto/config';
|
|
9
|
-
import { castTo } from '@travetto/runtime';
|
|
10
6
|
|
|
11
7
|
import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
|
|
12
8
|
import { WebInterceptorCategory } from '../types/core.ts';
|
|
13
9
|
import { WebChainedContext } from '../types/filter.ts';
|
|
14
10
|
import { WebResponse } from '../types/response.ts';
|
|
15
|
-
import { WebBodyUtil } from '../util/body.ts';
|
|
16
11
|
import { WebError } from '../types/error.ts';
|
|
17
12
|
|
|
13
|
+
import { WebBodyUtil } from '../util/body.ts';
|
|
14
|
+
import { WebHeaderUtil } from '../util/header.ts';
|
|
15
|
+
|
|
18
16
|
const COMPRESSORS = {
|
|
19
17
|
gzip: createGzip,
|
|
20
18
|
deflate: createDeflate,
|
|
@@ -33,10 +31,6 @@ export class CompressConfig {
|
|
|
33
31
|
* Raw encoding options
|
|
34
32
|
*/
|
|
35
33
|
raw?: (ZlibOptions & BrotliOptions) | undefined;
|
|
36
|
-
/**
|
|
37
|
-
* Preferred encodings
|
|
38
|
-
*/
|
|
39
|
-
preferredEncodings?: WebCompressEncoding[] = ['br', 'gzip', 'identity'];
|
|
40
34
|
/**
|
|
41
35
|
* Supported encodings
|
|
42
36
|
*/
|
|
@@ -55,10 +49,10 @@ export class CompressInterceptor implements WebInterceptor {
|
|
|
55
49
|
config: CompressConfig;
|
|
56
50
|
|
|
57
51
|
async compress(ctx: WebChainedContext, response: WebResponse): Promise<WebResponse> {
|
|
58
|
-
const { raw = {},
|
|
52
|
+
const { raw = {}, supportedEncodings } = this.config;
|
|
59
53
|
const { request } = ctx;
|
|
60
54
|
|
|
61
|
-
response.headers.
|
|
55
|
+
response.headers.append('Vary', 'Accept-Encoding');
|
|
62
56
|
|
|
63
57
|
if (
|
|
64
58
|
!response.body ||
|
|
@@ -69,15 +63,13 @@ export class CompressInterceptor implements WebInterceptor {
|
|
|
69
63
|
}
|
|
70
64
|
|
|
71
65
|
const accepts = request.headers.get('Accept-Encoding');
|
|
72
|
-
const type
|
|
73
|
-
castTo(new Negotiator({ headers: { 'accept-encoding': accepts ?? '*' } })
|
|
74
|
-
.encoding([...supportedEncodings, ...preferredEncodings]));
|
|
66
|
+
const type = WebHeaderUtil.negotiateHeader(accepts ?? '*', supportedEncodings);
|
|
75
67
|
|
|
76
|
-
if (
|
|
68
|
+
if (!type) {
|
|
77
69
|
throw WebError.for(`Please accept one of: ${supportedEncodings.join(', ')}. ${accepts} is not supported`, 406);
|
|
78
70
|
}
|
|
79
71
|
|
|
80
|
-
if (type === 'identity'
|
|
72
|
+
if (type === 'identity') {
|
|
81
73
|
return response;
|
|
82
74
|
}
|
|
83
75
|
|
package/src/interceptor/etag.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
import fresh from 'fresh';
|
|
3
2
|
|
|
4
3
|
import { Injectable, Inject } from '@travetto/di';
|
|
5
4
|
import { Config } from '@travetto/config';
|
|
@@ -13,6 +12,7 @@ import { WebInterceptorCategory } from '../types/core.ts';
|
|
|
13
12
|
import { CompressInterceptor } from './compress.ts';
|
|
14
13
|
import { WebBodyUtil } from '../util/body.ts';
|
|
15
14
|
import { ByteSizeInput, WebCommonUtil } from '../util/common.ts';
|
|
15
|
+
import { WebHeaderUtil } from '../util/header.ts';
|
|
16
16
|
|
|
17
17
|
@Config('web.etag')
|
|
18
18
|
export class EtagConfig {
|
|
@@ -85,17 +85,7 @@ export class EtagInterceptor implements WebInterceptor {
|
|
|
85
85
|
const tag = this.computeTag(body);
|
|
86
86
|
binaryResponse.headers.set('ETag', `${ctx.config.weak ? 'W/' : ''}"${tag}"`);
|
|
87
87
|
|
|
88
|
-
if (
|
|
89
|
-
ctx.config.cacheable &&
|
|
90
|
-
fresh({
|
|
91
|
-
'if-modified-since': request.headers.get('If-Modified-Since')!,
|
|
92
|
-
'if-none-match': request.headers.get('If-None-Match')!,
|
|
93
|
-
'cache-control': request.headers.get('Cache-Control')!,
|
|
94
|
-
}, {
|
|
95
|
-
etag: binaryResponse.headers.get('ETag')!,
|
|
96
|
-
'last-modified': binaryResponse.headers.get('Last-Modified')!
|
|
97
|
-
})
|
|
98
|
-
) {
|
|
88
|
+
if (ctx.config.cacheable && WebHeaderUtil.isFresh(request.headers, binaryResponse.headers)) {
|
|
99
89
|
// Remove length for the 304
|
|
100
90
|
binaryResponse.headers.delete('Content-Length');
|
|
101
91
|
return new WebResponse({
|
package/src/types/cookie.ts
CHANGED
package/src/types/headers.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
import { Any,
|
|
2
|
-
import { MimeType, MimeUtil } from '../util/mime.ts';
|
|
1
|
+
import { Any, castTo } from '@travetto/runtime';
|
|
3
2
|
|
|
4
3
|
type Prim = number | boolean | string;
|
|
5
4
|
type HeaderValue = Prim | Prim[] | readonly Prim[];
|
|
6
5
|
export type WebHeadersInit = Headers | Record<string, undefined | null | HeaderValue> | [string, HeaderValue][];
|
|
7
6
|
|
|
8
|
-
const FILENAME_EXTRACT = /filename[*]?=["]?([^";]*)["]?/;
|
|
9
|
-
|
|
10
7
|
/**
|
|
11
8
|
* Simple Headers wrapper with additional logic for common patterns
|
|
12
9
|
*/
|
|
13
10
|
export class WebHeaders extends Headers {
|
|
14
11
|
|
|
15
|
-
#parsedType?: MimeType;
|
|
16
|
-
|
|
17
12
|
constructor(o?: WebHeadersInit) {
|
|
18
13
|
const passed = (o instanceof Headers);
|
|
19
14
|
super(passed ? o : undefined);
|
|
@@ -56,42 +51,6 @@ export class WebHeaders extends Headers {
|
|
|
56
51
|
}
|
|
57
52
|
}
|
|
58
53
|
|
|
59
|
-
/**
|
|
60
|
-
* Vary a value
|
|
61
|
-
*/
|
|
62
|
-
vary(value: string): void {
|
|
63
|
-
this.append('Vary', value);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Get the fully parsed content type
|
|
68
|
-
*/
|
|
69
|
-
getContentType(): MimeType | undefined {
|
|
70
|
-
return this.#parsedType ??= MimeUtil.parse(this.get('Content-Type')!);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Read the filename from the content disposition
|
|
75
|
-
*/
|
|
76
|
-
getFilename(): string | undefined {
|
|
77
|
-
const [, match] = (this.get('Content-Disposition') ?? '').match(FILENAME_EXTRACT) ?? [];
|
|
78
|
-
return match;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Get requested byte range for a given request
|
|
83
|
-
*/
|
|
84
|
-
getRange(chunkSize: number = 100 * 1024): ByteRange | undefined {
|
|
85
|
-
const rangeHeader = this.get('Range');
|
|
86
|
-
if (rangeHeader) {
|
|
87
|
-
const [start, end] = rangeHeader.replace(/bytes=/, '').split('-')
|
|
88
|
-
.map(x => x ? parseInt(x, 10) : undefined);
|
|
89
|
-
if (start !== undefined) {
|
|
90
|
-
return { start, end: end ?? (start + chunkSize) };
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
54
|
/**
|
|
96
55
|
* Set header value with a prefix
|
|
97
56
|
*/
|
package/src/util/body.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { TextDecoder } from 'node:util';
|
|
2
2
|
import { Readable } from 'node:stream';
|
|
3
|
-
import { buffer as toBuffer } from 'node:stream/consumers';
|
|
4
3
|
|
|
5
4
|
import { Any, BinaryUtil, castTo, hasToJSON, Util } from '@travetto/runtime';
|
|
6
5
|
|
|
@@ -15,20 +14,6 @@ const WebRawStreamSymbol = Symbol();
|
|
|
15
14
|
*/
|
|
16
15
|
export class WebBodyUtil {
|
|
17
16
|
|
|
18
|
-
/**
|
|
19
|
-
* Convert a node binary input to a buffer
|
|
20
|
-
*/
|
|
21
|
-
static async toBuffer(src: WebBinaryBody): Promise<Buffer> {
|
|
22
|
-
return Buffer.isBuffer(src) ? src : toBuffer(src);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Convert a node binary input to a readable
|
|
27
|
-
*/
|
|
28
|
-
static toReadable(src: WebBinaryBody): Readable {
|
|
29
|
-
return Buffer.isBuffer(src) ? Readable.from(src) : src;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
17
|
/**
|
|
33
18
|
* Generate multipart body
|
|
34
19
|
*/
|
package/src/util/common.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppError, ErrorCategory } from '@travetto/runtime';
|
|
1
|
+
import { AppError, ErrorCategory, Util } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import { WebResponse } from '../types/response.ts';
|
|
4
4
|
import { WebRequest } from '../types/request.ts';
|
|
@@ -31,6 +31,12 @@ export class WebCommonUtil {
|
|
|
31
31
|
gb: 2 ** 30,
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
static #convert(rule: string): RegExp {
|
|
35
|
+
const core = (rule.endsWith('/*') || !rule.includes('/')) ?
|
|
36
|
+
`${rule.replace(/[/].{0,20}$/, '')}\/.*` : rule;
|
|
37
|
+
return new RegExp(`^${core}[ ]{0,10}(;|$)`);
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
static #buildEdgeMap<T, U extends OrderedState<T>>(items: List<U>): Map<T, Set<T>> {
|
|
35
41
|
const edgeMap = new Map(items.map(x => [x.key, new Set(x.after ?? [])]));
|
|
36
42
|
|
|
@@ -49,6 +55,19 @@ export class WebCommonUtil {
|
|
|
49
55
|
return edgeMap;
|
|
50
56
|
}
|
|
51
57
|
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build matcher
|
|
61
|
+
*/
|
|
62
|
+
static mimeTypeMatcher(rules: string[] | string = []): (contentType: string) => boolean {
|
|
63
|
+
return Util.allowDeny<RegExp, [string]>(
|
|
64
|
+
rules,
|
|
65
|
+
this.#convert.bind(this),
|
|
66
|
+
(regex, mime) => regex.test(mime),
|
|
67
|
+
k => k
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
/**
|
|
53
72
|
* Produces a satisfied ordering for a list of orderable elements
|
|
54
73
|
*/
|
package/src/util/cookie.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { AppError, castKey, castTo } from '@travetto/runtime';
|
|
1
|
+
import { AppError } from '@travetto/runtime';
|
|
3
2
|
|
|
4
3
|
import { Cookie, CookieGetOptions, CookieSetOptions } from '../types/cookie.ts';
|
|
4
|
+
import { KeyGrip } from './keygrip.ts';
|
|
5
|
+
import { WebHeaderUtil } from './header.ts';
|
|
5
6
|
|
|
6
7
|
const pairText = (c: Cookie): string => `${c.name}=${c.value}`;
|
|
7
8
|
const pair = (k: string, v: unknown): string => `${k}=${v}`;
|
|
@@ -10,55 +11,13 @@ type CookieJarOptions = { keys?: string[] } & CookieSetOptions;
|
|
|
10
11
|
|
|
11
12
|
export class CookieJar {
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
return header.split(/\s{0,4};\s{0,4}/g)
|
|
15
|
-
.map(x => x.trim())
|
|
16
|
-
.filter(x => !!x)
|
|
17
|
-
.map(item => {
|
|
18
|
-
const kv = item.split(/\s{0,4}=\s{0,4}/);
|
|
19
|
-
return { name: kv[0], value: kv[1] };
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
static parseSetCookieHeader(header: string): Cookie {
|
|
24
|
-
const parts = header.split(/\s{0,4};\s{0,4}/g);
|
|
25
|
-
const [name, value] = parts[0].split(/\s{0,4}=\s{0,4}/);
|
|
26
|
-
const c: Cookie = { name, value };
|
|
27
|
-
for (const p of parts.slice(1)) {
|
|
28
|
-
// eslint-disable-next-line prefer-const
|
|
29
|
-
let [k, v = ''] = p.split(/\s{0,4}=\s{0,4}/);
|
|
30
|
-
if (v[0] === '"') {
|
|
31
|
-
v = v.slice(1, -1);
|
|
32
|
-
}
|
|
33
|
-
if (k === 'expires') {
|
|
34
|
-
c[k] = new Date(v);
|
|
35
|
-
} else {
|
|
36
|
-
c[castKey(k)] = castTo(v || true);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return c;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
static responseSuffix(c: Cookie): string[] {
|
|
43
|
-
const parts = [];
|
|
44
|
-
if (c.path) { parts.push(pair('path', c.path)); }
|
|
45
|
-
if (c.expires) { parts.push(pair('expires', c.expires.toUTCString())); }
|
|
46
|
-
if (c.domain) { parts.push(pair('domain', c.domain)); }
|
|
47
|
-
if (c.priority) { parts.push(pair('priority', c.priority.toLowerCase())); }
|
|
48
|
-
if (c.sameSite) { parts.push(pair('samesite', c.sameSite.toLowerCase())); }
|
|
49
|
-
if (c.secure) { parts.push('secure'); }
|
|
50
|
-
if (c.httpOnly) { parts.push('httponly'); }
|
|
51
|
-
if (c.partitioned) { parts.push('partitioned'); }
|
|
52
|
-
return parts;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
#grip?: keygrip;
|
|
14
|
+
#grip?: KeyGrip;
|
|
56
15
|
#cookies: Record<string, Cookie> = {};
|
|
57
16
|
#setOptions: CookieSetOptions = {};
|
|
58
17
|
#deleteOptions: CookieSetOptions = { maxAge: 0, expires: undefined };
|
|
59
18
|
|
|
60
19
|
constructor({ keys, ...options }: CookieJarOptions = {}) {
|
|
61
|
-
this.#grip = keys?.length ? new
|
|
20
|
+
this.#grip = keys?.length ? new KeyGrip(keys) : undefined;
|
|
62
21
|
this.#setOptions = {
|
|
63
22
|
secure: false,
|
|
64
23
|
path: '/',
|
|
@@ -68,7 +27,7 @@ export class CookieJar {
|
|
|
68
27
|
}
|
|
69
28
|
|
|
70
29
|
#exportCookie(cookie: Cookie, response?: boolean): string[] {
|
|
71
|
-
const suffix = response ?
|
|
30
|
+
const suffix = response ? WebHeaderUtil.buildCookieSuffix(cookie) : null;
|
|
72
31
|
const payload = pairText(cookie);
|
|
73
32
|
const out = suffix ? [[payload, ...suffix].join(';')] : [payload];
|
|
74
33
|
if (cookie.signed) {
|
|
@@ -144,11 +103,11 @@ export class CookieJar {
|
|
|
144
103
|
}
|
|
145
104
|
|
|
146
105
|
importCookieHeader(header: string | null | undefined): this {
|
|
147
|
-
return this.import(
|
|
106
|
+
return this.import(WebHeaderUtil.parseCookieHeader(header ?? ''));
|
|
148
107
|
}
|
|
149
108
|
|
|
150
109
|
importSetCookieHeader(headers: string[] | null | undefined): this {
|
|
151
|
-
return this.import(headers?.map(
|
|
110
|
+
return this.import(headers?.map(WebHeaderUtil.parseSetCookieHeader) ?? []);
|
|
152
111
|
}
|
|
153
112
|
|
|
154
113
|
exportCookieHeader(): string {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { ByteRange, castKey, castTo } from '@travetto/runtime';
|
|
2
|
+
|
|
3
|
+
import { type Cookie } from '../types/cookie.ts';
|
|
4
|
+
import { WebHeaders } from '../types/headers.ts';
|
|
5
|
+
|
|
6
|
+
export type WebParsedHeader = { value: string, parameters: Record<string, string>, q?: number };
|
|
7
|
+
|
|
8
|
+
const SPLIT_EQ = /[ ]{0,10}=[ ]{0,10}/g;
|
|
9
|
+
const SPLIT_COMMA = /[ ]{0,10},[ ]{0,10}/g;
|
|
10
|
+
const SPLIT_SEMI = /[ ]{0,10};[ ]{0,10}/g;
|
|
11
|
+
const QUOTE = '"'.charCodeAt(0);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Web header utils
|
|
15
|
+
*/
|
|
16
|
+
export class WebHeaderUtil {
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse cookie header
|
|
20
|
+
*/
|
|
21
|
+
static parseCookieHeader(header: string): Cookie[] {
|
|
22
|
+
const val = header.trim();
|
|
23
|
+
return !val ? [] : val.split(SPLIT_SEMI).map(item => {
|
|
24
|
+
const [name, value] = item.split(SPLIT_EQ);
|
|
25
|
+
return { name, value };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse cookie set header
|
|
31
|
+
*/
|
|
32
|
+
static parseSetCookieHeader(header: string): Cookie {
|
|
33
|
+
const parts = header.split(SPLIT_SEMI);
|
|
34
|
+
const [name, value] = parts[0].split(SPLIT_EQ);
|
|
35
|
+
const c: Cookie = { name, value };
|
|
36
|
+
for (const p of parts.slice(1)) {
|
|
37
|
+
const [k, pv = ''] = p.toLowerCase().split(SPLIT_EQ);
|
|
38
|
+
const v = pv.charCodeAt(0) === QUOTE ? pv.slice(1, -1) : pv;
|
|
39
|
+
if (k === 'expires') {
|
|
40
|
+
c[k] = new Date(v);
|
|
41
|
+
} else {
|
|
42
|
+
c[castKey(k)] = castTo(v || true);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return c;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse header segment
|
|
50
|
+
* @input input
|
|
51
|
+
*/
|
|
52
|
+
static parseHeaderSegment(input: string | null | undefined): WebParsedHeader {
|
|
53
|
+
if (!input) {
|
|
54
|
+
return { value: '', parameters: {} };
|
|
55
|
+
}
|
|
56
|
+
const [rv, ...parts] = input.split(SPLIT_SEMI);
|
|
57
|
+
const item: WebParsedHeader = { value: '', parameters: {} };
|
|
58
|
+
const value = rv.charCodeAt(0) === QUOTE ? rv.slice(1, -1) : rv;
|
|
59
|
+
if (value.includes('=')) {
|
|
60
|
+
parts.unshift(value);
|
|
61
|
+
} else {
|
|
62
|
+
item.value = value;
|
|
63
|
+
}
|
|
64
|
+
for (const part of parts) {
|
|
65
|
+
const [k, pv = ''] = part.split(SPLIT_EQ);
|
|
66
|
+
const v = (pv.charCodeAt(0) === QUOTE) ? pv.slice(1, -1) : pv;
|
|
67
|
+
item.parameters[k] = v;
|
|
68
|
+
if (k === 'q') {
|
|
69
|
+
item.q = parseFloat(v);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return item;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse full header
|
|
77
|
+
*/
|
|
78
|
+
static parseHeader(input: string): WebParsedHeader[] {
|
|
79
|
+
const v = input.trim();
|
|
80
|
+
if (!input) { return []; }
|
|
81
|
+
return v.split(SPLIT_COMMA).map(x => this.parseHeaderSegment(x));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build cookie suffix
|
|
86
|
+
*/
|
|
87
|
+
static buildCookieSuffix(c: Cookie): string[] {
|
|
88
|
+
const parts = [];
|
|
89
|
+
if (c.path) { parts.push(`path=${c.path}`); }
|
|
90
|
+
if (c.expires) { parts.push(`expires=${c.expires.toUTCString()}`); }
|
|
91
|
+
if (c.domain) { parts.push(`domain=${c.domain}`); }
|
|
92
|
+
if (c.priority) { parts.push(`priority=${c.priority.toLowerCase()}`); }
|
|
93
|
+
if (c.sameSite) { parts.push(`samesite=${c.sameSite.toLowerCase()}`); }
|
|
94
|
+
if (c.secure) { parts.push('secure'); }
|
|
95
|
+
if (c.httponly) { parts.push('httponly'); }
|
|
96
|
+
if (c.partitioned) { parts.push('partitioned'); }
|
|
97
|
+
return parts;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Negotiate header
|
|
102
|
+
*/
|
|
103
|
+
static negotiateHeader<K extends string>(header: string, values: K[]): K | undefined {
|
|
104
|
+
if (header === '*' || header === '*/*') {
|
|
105
|
+
return values[0];
|
|
106
|
+
}
|
|
107
|
+
const sorted = this.parseHeader(header.toLowerCase()).filter(x => (x.q ?? 1) > 0).toSorted((a, b) => (b.q ?? 1) - (a.q ?? 1));
|
|
108
|
+
const set = new Set(values);
|
|
109
|
+
for (const { value } of sorted) {
|
|
110
|
+
const vk: K = castKey(value);
|
|
111
|
+
if (value === '*') {
|
|
112
|
+
return values[0];
|
|
113
|
+
} else if (set.has(vk)) {
|
|
114
|
+
return vk;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get requested byte range for a given request
|
|
122
|
+
*/
|
|
123
|
+
static getRange(headers: WebHeaders, chunkSize: number = 100 * 1024): ByteRange | undefined {
|
|
124
|
+
if (!headers.has('Range')) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const { parameters } = this.parseHeaderSegment(headers.get('Range'));
|
|
128
|
+
if ('bytes' in parameters) {
|
|
129
|
+
const [start, end] = parameters.bytes.split('-')
|
|
130
|
+
.map(x => x ? parseInt(x, 10) : undefined);
|
|
131
|
+
if (start !== undefined) {
|
|
132
|
+
return { start, end: end ?? (start + chunkSize) };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check freshness of the response using request and response headers.
|
|
139
|
+
*/
|
|
140
|
+
static isFresh(req: WebHeaders, res: WebHeaders): boolean {
|
|
141
|
+
const cacheControl = req.get('Cache-Control');
|
|
142
|
+
if (cacheControl?.includes('no-cache')) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const noneMatch = req.get('If-None-Match');
|
|
147
|
+
if (noneMatch) {
|
|
148
|
+
const etag = res.get('ETag');
|
|
149
|
+
const validTag = (v: string): boolean => v === etag || v === `W/${etag}` || `W/${v}` === etag;
|
|
150
|
+
return noneMatch === '*' || (!!etag && noneMatch.split(SPLIT_COMMA).some(validTag));
|
|
151
|
+
} else {
|
|
152
|
+
const modifiedSince = req.get('If-Modified-Since');
|
|
153
|
+
const lastModified = res.get('Last-Modified');
|
|
154
|
+
if (!modifiedSince || !lastModified) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const [a, b] = [Date.parse(lastModified), Date.parse(modifiedSince)];
|
|
158
|
+
return !(Number.isNaN(a) || Number.isNaN(b)) && a >= b;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import crypto, { BinaryToTextEncoding } from 'node:crypto';
|
|
2
|
+
import { AppError, castKey } from '@travetto/runtime';
|
|
3
|
+
|
|
4
|
+
const CHAR_MAPPING = { '/': '_', '+': '-', '=': '' };
|
|
5
|
+
|
|
6
|
+
function timeSafeCompare(a: string, b: string): boolean {
|
|
7
|
+
const key = crypto.randomBytes(32);
|
|
8
|
+
const ah = crypto.createHmac('sha256', key).update(a).digest();
|
|
9
|
+
const bh = crypto.createHmac('sha256', key).update(b).digest();
|
|
10
|
+
return ah.length === bh.length && crypto.timingSafeEqual(ah, bh);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class KeyGrip {
|
|
14
|
+
|
|
15
|
+
#keys: string[];
|
|
16
|
+
#algorithm: string;
|
|
17
|
+
#encoding: BinaryToTextEncoding;
|
|
18
|
+
|
|
19
|
+
constructor(keys: string[], algorithm = 'sha1', encoding: BinaryToTextEncoding = 'base64') {
|
|
20
|
+
if (!keys.length) {
|
|
21
|
+
throw new AppError('Keys must be defined');
|
|
22
|
+
}
|
|
23
|
+
this.#keys = keys;
|
|
24
|
+
this.#algorithm = algorithm;
|
|
25
|
+
this.#encoding = encoding;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
sign(data: string, key?: string): string {
|
|
29
|
+
return crypto
|
|
30
|
+
.createHmac(this.#algorithm, key ?? this.#keys[0])
|
|
31
|
+
.update(data)
|
|
32
|
+
.digest(this.#encoding)
|
|
33
|
+
.replace(/[/+=]/g, x => CHAR_MAPPING[castKey(x)]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
verify(data: string, digest: string): boolean {
|
|
37
|
+
return this.index(data, digest) > -1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
index(data: string, digest: string): number {
|
|
41
|
+
return this.#keys.findIndex(key => timeSafeCompare(digest, this.sign(data, key)));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { buffer
|
|
1
|
+
import { buffer } from 'node:stream/consumers';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
2
3
|
|
|
3
4
|
import { AppError, BinaryUtil, castTo } from '@travetto/runtime';
|
|
4
5
|
import { BindUtil } from '@travetto/schema';
|
|
@@ -9,6 +10,8 @@ import { DecompressInterceptor } from '../../src/interceptor/decompress.ts';
|
|
|
9
10
|
import { WebBodyUtil } from '../../src/util/body.ts';
|
|
10
11
|
import { WebCommonUtil } from '../../src/util/common.ts';
|
|
11
12
|
|
|
13
|
+
const toBuffer = (src: Buffer | Readable) => Buffer.isBuffer(src) ? src : buffer(src);
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Utilities for supporting custom test dispatchers
|
|
14
17
|
*/
|
|
@@ -18,7 +21,7 @@ export class WebTestDispatchUtil {
|
|
|
18
21
|
if (request.body !== undefined) {
|
|
19
22
|
const sample = WebBodyUtil.toBinaryMessage(request);
|
|
20
23
|
sample.headers.forEach((v, k) => request.headers.set(k, Array.isArray(v) ? v.join(',') : v));
|
|
21
|
-
request.body = WebBodyUtil.markRaw(await
|
|
24
|
+
request.body = WebBodyUtil.markRaw(await toBuffer(sample.body!));
|
|
22
25
|
}
|
|
23
26
|
Object.assign(request.context, { httpQuery: BindUtil.flattenPaths(request.context.httpQuery ?? {}) });
|
|
24
27
|
return request;
|
|
@@ -31,7 +34,7 @@ export class WebTestDispatchUtil {
|
|
|
31
34
|
|
|
32
35
|
if (decompress) {
|
|
33
36
|
if (Buffer.isBuffer(result) || BinaryUtil.isReadable(result)) {
|
|
34
|
-
const bufferResult = result = await
|
|
37
|
+
const bufferResult = result = await toBuffer(result);
|
|
35
38
|
if (bufferResult.length) {
|
|
36
39
|
try {
|
|
37
40
|
result = await DecompressInterceptor.decompress(
|
|
@@ -74,7 +77,7 @@ export class WebTestDispatchUtil {
|
|
|
74
77
|
|
|
75
78
|
const body: RequestInit['body'] =
|
|
76
79
|
WebBodyUtil.isRaw(request.body) ?
|
|
77
|
-
|
|
80
|
+
await toBuffer(request.body) :
|
|
78
81
|
castTo(request.body);
|
|
79
82
|
|
|
80
83
|
return { path: finalPath, init: { headers, method, body } };
|
package/src/util/mime.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { Util } from '@travetto/runtime';
|
|
2
|
-
|
|
3
|
-
export type MimeType = { type: string, subtype: string, full: string, parameters: Record<string, string> };
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Utils for checking mime patterns
|
|
7
|
-
*/
|
|
8
|
-
export class MimeUtil {
|
|
9
|
-
|
|
10
|
-
static #convert(rule: string): RegExp {
|
|
11
|
-
const core = (rule.endsWith('/*') || !rule.includes('/')) ?
|
|
12
|
-
`${rule.replace(/[/].{0,20}$/, '')}\/.*` : rule;
|
|
13
|
-
return new RegExp(`^${core}[ ]{0,10}(;|$)`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
static parse(mimeType?: string): MimeType | undefined {
|
|
17
|
-
if (mimeType) {
|
|
18
|
-
const [full, ...params] = mimeType.split(/;/).map(x => x.trim());
|
|
19
|
-
const [type, subtype] = full.split('/');
|
|
20
|
-
const parameters = Object.fromEntries(params.map(v => v.split('=')).map(([k, v]) => [k.toLowerCase(), v]));
|
|
21
|
-
return { type, subtype, full, parameters };
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Build matcher
|
|
27
|
-
*/
|
|
28
|
-
static matcher(rules: string[] | string = []): (contentType: string) => boolean {
|
|
29
|
-
return Util.allowDeny<RegExp, [string]>(
|
|
30
|
-
rules,
|
|
31
|
-
this.#convert.bind(this),
|
|
32
|
-
(regex, mime) => regex.test(mime),
|
|
33
|
-
k => k
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
}
|