-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
HashMiddleware.js
109 lines (98 loc) · 3.51 KB
/
HashMiddleware.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import { createHash } from 'node:crypto';
import { Transform } from 'node:stream';
/** @typedef {import('node:crypto').BinaryToTextEncoding} BinaryToTextEncoding */
/** @typedef {import('../lib/HttpRequest.js').default} HttpRequest */
/** @typedef {import('../lib/HttpResponse.js').default} HttpResponse */
/** @typedef {import('../types/index.js').MiddlewareFunction} MiddlewareFunction */
/** @typedef {import('../types/index.js').ResponseFinalizer} ResponseFinalizer */
const DEFAULT_ALGORITHM = 'sha1';
/** @type {BinaryToTextEncoding} */
const DEFAULT_DIGEST = 'base64';
/**
* @typedef {Object} HashMiddlewareOptions
* @prop {'md5'|'sha1'|'sha256'|'sha512'} [algorithm=DEFAULT_ALGORITHM]
* @prop {BinaryToTextEncoding} [digest=DEFAULT_DIGEST]
*/
export default class HashMiddleware {
/** @param {HashMiddlewareOptions} options */
constructor(options = {}) {
this.algorithm = options.algorithm || DEFAULT_ALGORITHM;
this.digest = options.digest || DEFAULT_DIGEST;
this.finalizeResponse = this.finalizeResponse.bind(this);
}
/**
* @param {HttpResponse} response
* @return {void}
*/
addTransformStream(response) {
if (response.headers.etag != null || response.headers.digest != null) return;
const { algorithm, digest } = this;
let hasData = false;
let length = 0;
let abort = false;
const hashStream = createHash(algorithm);
response.pipes.push(new Transform({
objectMode: true,
transform(chunk, encoding, callback) {
length += chunk.length;
hasData = true;
if (!abort) {
if (response.headersSent) {
abort = true;
hashStream.destroy();
} else {
// Manually pipe
const isSync = hashStream.write(chunk, (err) => {
if (!isSync) {
callback(err, chunk);
}
});
if (!isSync) return;
}
}
callback(null, chunk);
},
final(callback) {
if (!abort && hasData && response.status !== 206 && !response.headersSent) {
const hash = hashStream.digest(digest);
// https://tools.ietf.org/html/rfc7232#section-2.3
if (response.headers.etag == null) {
response.headers.etag = `${algorithm === 'md5' ? 'W/' : ''}"${length.toString(16)}-${hash}"`;
}
if (digest === 'base64') {
response.headers.digest = `${algorithm}=${hash}`;
if ((algorithm === 'md5')) {
response.headers['content-md5'] = hash;
}
}
}
callback();
},
}));
}
/** @type {ResponseFinalizer} */
finalizeResponse(response) {
if (response.status === 206 || response.body == null) return;
if (response.isStreaming) {
this.addTransformStream(response);
return;
}
if (!Buffer.isBuffer(response.body) || !response.body.byteLength) return;
const { algorithm, digest } = this;
const hash = createHash(algorithm).update(response.body).digest(digest);
// https://tools.ietf.org/html/rfc7232#section-2.3
if (response.headers.etag == null) {
response.headers.etag = `${algorithm === 'md5' ? 'W/' : ''}"${response.body.byteLength.toString(16)}-${hash}"`;
}
if (digest === 'base64') {
response.headers.digest = `${algorithm}=${hash}`;
if ((algorithm === 'md5')) {
response.headers['content-md5'] = hash;
}
}
}
/** @type {MiddlewareFunction} */
execute({ response }) {
response.finalizers.push(this.finalizeResponse);
}
}