s3-test-server 0.0.0-stage → 1.0.1

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.
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const { S3Error } = require('../models/error');
4
+
5
+ function checkAccessKey(accessKeyId, options) {
6
+ if (accessKeyId !== options.accessKeyId) {
7
+ throw new S3Error(
8
+ 'InvalidAccessKeyId',
9
+ 'The AWS Access Key Id you provided does not exist in our records.',
10
+ { AWSAccessKeyId: accessKeyId }
11
+ );
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Returns the SignatureDoesNotMatch error to throw, or null when the server
17
+ * runs with `allowMismatchedSignatures`.
18
+ */
19
+ function signatureMismatch(options, detail) {
20
+ if (options.allowMismatchedSignatures) return null;
21
+ return new S3Error(
22
+ 'SignatureDoesNotMatch',
23
+ 'The request signature we calculated does not match the signature you provided. Check your key and signing method.',
24
+ detail
25
+ );
26
+ }
27
+
28
+ module.exports = { checkAccessKey, signatureMismatch };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const { S3Error } = require('../models/error');
4
+ const { verifyV2 } = require('./v2');
5
+ const { verifyV4Header, verifyV4Query } = require('./v4');
6
+
7
+ /**
8
+ * Authenticates a request. Supports:
9
+ * - SigV4 Authorization header (AWS4-HMAC-SHA256)
10
+ * - SigV4 presigned query (X-Amz-Algorithm=AWS4-HMAC-SHA256)
11
+ * - SigV2 Authorization header (AWS AKID:signature)
12
+ * - SigV2 presigned query (AWSAccessKeyId/Expires/Signature)
13
+ * - anonymous requests
14
+ *
15
+ * Returns { mode, accessKeyId, contentSha256 } where contentSha256 is the
16
+ * declared payload hash to verify against the actual body (SigV4 only), or
17
+ * null when no verification applies.
18
+ */
19
+ function authenticate({ req, rawPath, query, vhostBucket, options }) {
20
+ const authHeader = req.headers.authorization;
21
+ if (authHeader) {
22
+ if (authHeader.startsWith('AWS4-HMAC-SHA256')) {
23
+ return verifyV4Header({ req, rawPath, query, authHeader, options });
24
+ }
25
+ if (authHeader.startsWith('AWS ')) {
26
+ return verifyV2({
27
+ req,
28
+ rawPath,
29
+ query,
30
+ vhostBucket,
31
+ options,
32
+ authHeader,
33
+ });
34
+ }
35
+ throw new S3Error('InvalidArgument', 'Unsupported Authorization Type', {
36
+ ArgumentName: 'Authorization',
37
+ ArgumentValue: authHeader,
38
+ });
39
+ }
40
+ if (query.get('X-Amz-Algorithm')) {
41
+ return verifyV4Query({ req, rawPath, query, options });
42
+ }
43
+ if (query.get('Signature') && query.get('AWSAccessKeyId')) {
44
+ return verifyV2({ req, rawPath, query, vhostBucket, options });
45
+ }
46
+ return { mode: 'anonymous', accessKeyId: null, contentSha256: null };
47
+ }
48
+
49
+ module.exports = { authenticate };
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { S3Error } = require('../models/error');
5
+ const { timingSafeEqualStr } = require('../utils');
6
+ const { checkAccessKey, signatureMismatch } = require('./common');
7
+
8
+ // Sub-resources included in the SigV2 canonicalized resource, per the
9
+ // AWS Signature Version 2 documentation.
10
+ const V2_SUBRESOURCES = new Set([
11
+ 'acl',
12
+ 'accelerate',
13
+ 'analytics',
14
+ 'cors',
15
+ 'delete',
16
+ 'encryption',
17
+ 'inventory',
18
+ 'lifecycle',
19
+ 'location',
20
+ 'logging',
21
+ 'metrics',
22
+ 'notification',
23
+ 'object-lock',
24
+ 'partNumber',
25
+ 'policy',
26
+ 'publicAccessBlock',
27
+ 'replication',
28
+ 'requestPayment',
29
+ 'restore',
30
+ 'tagging',
31
+ 'torrent',
32
+ 'uploadId',
33
+ 'uploads',
34
+ 'versionId',
35
+ 'versioning',
36
+ 'versions',
37
+ 'website',
38
+ 'response-content-type',
39
+ 'response-content-language',
40
+ 'response-expires',
41
+ 'response-cache-control',
42
+ 'response-content-disposition',
43
+ 'response-content-encoding',
44
+ ]);
45
+
46
+ function verifyV2({ req, rawPath, query, vhostBucket, options, authHeader }) {
47
+ let accessKeyId;
48
+ let signature;
49
+ let expires = null;
50
+ if (authHeader) {
51
+ const match = /^AWS\s+([^:]+):(.+)$/.exec(authHeader.trim());
52
+ if (!match) {
53
+ throw new S3Error('InvalidArgument', 'Authorization header is invalid', {
54
+ ArgumentName: 'Authorization',
55
+ ArgumentValue: authHeader,
56
+ });
57
+ }
58
+ [, accessKeyId, signature] = match;
59
+ } else {
60
+ accessKeyId = query.get('AWSAccessKeyId');
61
+ signature = query.get('Signature');
62
+ expires = query.get('Expires');
63
+ if (Date.now() > parseInt(expires, 10) * 1000) {
64
+ throw new S3Error('AccessDenied', 'Request has expired', {
65
+ Expires: expires,
66
+ ServerTime: new Date().toISOString(),
67
+ });
68
+ }
69
+ }
70
+ checkAccessKey(accessKeyId, options);
71
+
72
+ // Canonicalized x-amz-* headers
73
+ const amzHeaders = Object.keys(req.headers)
74
+ .filter((name) => name.startsWith('x-amz-'))
75
+ .sort()
76
+ .map((name) => {
77
+ const value = Array.isArray(req.headers[name])
78
+ ? req.headers[name].join(',')
79
+ : req.headers[name];
80
+ return `${name}:${String(value).trim()}\n`;
81
+ })
82
+ .join('');
83
+
84
+ // Canonicalized resource
85
+ let resource = vhostBucket ? `/${vhostBucket}${rawPath}` : rawPath;
86
+ const subresources = [];
87
+ for (const [name, value] of query.entries()) {
88
+ if (V2_SUBRESOURCES.has(name)) {
89
+ subresources.push(value === '' ? name : `${name}=${value}`);
90
+ }
91
+ }
92
+ if (subresources.length > 0) {
93
+ subresources.sort();
94
+ resource += '?' + subresources.join('&');
95
+ }
96
+
97
+ const dateField =
98
+ expires !== null
99
+ ? expires
100
+ : req.headers['x-amz-date']
101
+ ? ''
102
+ : req.headers.date || '';
103
+ const stringToSign = [
104
+ req.method,
105
+ req.headers['content-md5'] || '',
106
+ req.headers['content-type'] || '',
107
+ dateField,
108
+ amzHeaders + resource,
109
+ ].join('\n');
110
+ const expected = crypto
111
+ .createHmac('sha1', options.secretAccessKey)
112
+ .update(stringToSign, 'utf8')
113
+ .digest('base64');
114
+ if (!timingSafeEqualStr(expected, signature)) {
115
+ const err = signatureMismatch(options, {
116
+ AWSAccessKeyId: accessKeyId,
117
+ StringToSign: stringToSign,
118
+ SignatureProvided: signature,
119
+ });
120
+ if (err) throw err;
121
+ }
122
+ return {
123
+ mode: authHeader ? 'v2-header' : 'v2-query',
124
+ accessKeyId,
125
+ contentSha256: null,
126
+ };
127
+ }
128
+
129
+ module.exports = { verifyV2 };
@@ -0,0 +1,227 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { S3Error } = require('../models/error');
5
+ const { awsUriEncode, timingSafeEqualStr } = require('../utils');
6
+ const { checkAccessKey, signatureMismatch } = require('./common');
7
+
8
+ const MAX_SKEW_MS = 15 * 60 * 1000;
9
+
10
+ function hmac(key, data, encoding) {
11
+ return crypto.createHmac('sha256', key).update(data, 'utf8').digest(encoding);
12
+ }
13
+
14
+ function sha256Hex(data) {
15
+ return crypto.createHash('sha256').update(data).digest('hex');
16
+ }
17
+
18
+ function parseAmzDate(value) {
19
+ const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(
20
+ value || ''
21
+ );
22
+ if (!match) return NaN;
23
+ return Date.UTC(
24
+ +match[1],
25
+ +match[2] - 1,
26
+ +match[3],
27
+ +match[4],
28
+ +match[5],
29
+ +match[6]
30
+ );
31
+ }
32
+
33
+ function canonicalQueryString(query, { excludeSignature = false } = {}) {
34
+ const pairs = [];
35
+ for (const [name, value] of query.entries()) {
36
+ if (excludeSignature && name === 'X-Amz-Signature') continue;
37
+ pairs.push([awsUriEncode(name), awsUriEncode(value)]);
38
+ }
39
+ pairs.sort((a, b) =>
40
+ a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0
41
+ );
42
+ return pairs.map(([name, value]) => `${name}=${value}`).join('&');
43
+ }
44
+
45
+ function canonicalHeaders(req, signedHeaderNames) {
46
+ const lines = [];
47
+ for (const name of signedHeaderNames) {
48
+ let value = req.headers[name];
49
+ if (value === undefined) {
50
+ if (name === 'host') value = req.headers[':authority'] || '';
51
+ else value = '';
52
+ }
53
+ if (Array.isArray(value)) value = value.join(',');
54
+ lines.push(`${name}:${String(value).trim().replace(/\s+/g, ' ')}`);
55
+ }
56
+ return lines.join('\n') + '\n';
57
+ }
58
+
59
+ function computeV4Signature(secret, credDate, region, stringToSign) {
60
+ const kDate = hmac(`AWS4${secret}`, credDate);
61
+ const kRegion = hmac(kDate, region);
62
+ const kService = hmac(kRegion, 's3');
63
+ const kSigning = hmac(kService, 'aws4_request');
64
+ return hmac(kSigning, stringToSign, 'hex');
65
+ }
66
+
67
+ function toAmzDate(ms) {
68
+ return new Date(ms).toISOString().replace(/[-:]|\.\d{3}/g, '');
69
+ }
70
+
71
+ function verifyV4Header({ req, rawPath, query, authHeader, options }) {
72
+ const match =
73
+ /^AWS4-HMAC-SHA256\s+Credential=([^,\s]+),\s*SignedHeaders=([^,\s]+),\s*Signature=([0-9a-f]+)$/.exec(
74
+ authHeader.trim()
75
+ );
76
+ if (!match) {
77
+ throw new S3Error(
78
+ 'AuthorizationHeaderMalformed',
79
+ 'The authorization header is malformed.'
80
+ );
81
+ }
82
+ const [, credential, signedHeaders, signature] = match;
83
+ const credParts = credential.split('/');
84
+ if (
85
+ credParts.length !== 5 ||
86
+ credParts[3] !== 's3' ||
87
+ credParts[4] !== 'aws4_request'
88
+ ) {
89
+ throw new S3Error(
90
+ 'AuthorizationHeaderMalformed',
91
+ `The authorization header is malformed; the credential scope is incorrect.`
92
+ );
93
+ }
94
+ const [accessKeyId, credDate, region] = credParts;
95
+ checkAccessKey(accessKeyId, options);
96
+
97
+ const amzDate = req.headers['x-amz-date'];
98
+ const dateMs = amzDate
99
+ ? parseAmzDate(amzDate)
100
+ : Date.parse(req.headers.date || '');
101
+ if (Number.isNaN(dateMs)) {
102
+ throw new S3Error(
103
+ 'AccessDenied',
104
+ 'AWS authentication requires a valid Date or x-amz-date header'
105
+ );
106
+ }
107
+ if (Math.abs(Date.now() - dateMs) > MAX_SKEW_MS) {
108
+ throw new S3Error(
109
+ 'RequestTimeTooSkewed',
110
+ "The difference between the request time and the server's time is too large."
111
+ );
112
+ }
113
+
114
+ const contentSha256 = req.headers['x-amz-content-sha256'];
115
+ if (!contentSha256) {
116
+ throw new S3Error(
117
+ 'InvalidRequest',
118
+ 'Missing required header for this request: x-amz-content-sha256'
119
+ );
120
+ }
121
+
122
+ const timestamp = amzDate || toAmzDate(dateMs);
123
+ const scope = `${credDate}/${region}/s3/aws4_request`;
124
+ const signedHeaderNames = signedHeaders.split(';');
125
+ const canonicalRequest = [
126
+ req.method,
127
+ rawPath,
128
+ canonicalQueryString(query),
129
+ canonicalHeaders(req, signedHeaderNames),
130
+ signedHeaders,
131
+ contentSha256,
132
+ ].join('\n');
133
+ const stringToSign = [
134
+ 'AWS4-HMAC-SHA256',
135
+ timestamp,
136
+ scope,
137
+ sha256Hex(canonicalRequest),
138
+ ].join('\n');
139
+ const expected = computeV4Signature(
140
+ options.secretAccessKey,
141
+ credDate,
142
+ region,
143
+ stringToSign
144
+ );
145
+ if (!timingSafeEqualStr(expected, signature)) {
146
+ const err = signatureMismatch(options, {
147
+ AWSAccessKeyId: accessKeyId,
148
+ StringToSign: stringToSign,
149
+ SignatureProvided: signature,
150
+ CanonicalRequest: canonicalRequest,
151
+ });
152
+ if (err) throw err;
153
+ }
154
+ return { mode: 'v4-header', accessKeyId, contentSha256 };
155
+ }
156
+
157
+ function verifyV4Query({ req, rawPath, query, options }) {
158
+ const algorithm = query.get('X-Amz-Algorithm');
159
+ if (algorithm !== 'AWS4-HMAC-SHA256') {
160
+ throw new S3Error(
161
+ 'AuthorizationQueryParametersError',
162
+ 'X-Amz-Algorithm only supports "AWS4-HMAC-SHA256"'
163
+ );
164
+ }
165
+ const credential = query.get('X-Amz-Credential') || '';
166
+ const signature = query.get('X-Amz-Signature') || '';
167
+ const signedHeaders = query.get('X-Amz-SignedHeaders') || '';
168
+ const amzDate = query.get('X-Amz-Date') || '';
169
+ const expires = parseInt(query.get('X-Amz-Expires') || '', 10);
170
+ const credParts = credential.split('/');
171
+ if (credParts.length !== 5 || !signature || !signedHeaders || !amzDate) {
172
+ throw new S3Error(
173
+ 'AuthorizationQueryParametersError',
174
+ 'Query-string authentication version 4 requires the X-Amz-Algorithm, X-Amz-Credential, X-Amz-Signature, X-Amz-Date, X-Amz-SignedHeaders, and X-Amz-Expires parameters.'
175
+ );
176
+ }
177
+ const [accessKeyId, credDate, region] = credParts;
178
+ checkAccessKey(accessKeyId, options);
179
+
180
+ const dateMs = parseAmzDate(amzDate);
181
+ if (Number.isNaN(dateMs)) {
182
+ throw new S3Error(
183
+ 'AuthorizationQueryParametersError',
184
+ 'X-Amz-Date is malformed'
185
+ );
186
+ }
187
+ if (!Number.isNaN(expires) && Date.now() > dateMs + expires * 1000) {
188
+ throw new S3Error('AccessDenied', 'Request has expired', {
189
+ 'X-Amz-Expires': expires,
190
+ Expires: new Date(dateMs + expires * 1000).toISOString(),
191
+ ServerTime: new Date().toISOString(),
192
+ });
193
+ }
194
+
195
+ const signedHeaderNames = signedHeaders.split(';');
196
+ const canonicalRequest = [
197
+ req.method,
198
+ rawPath,
199
+ canonicalQueryString(query, { excludeSignature: true }),
200
+ canonicalHeaders(req, signedHeaderNames),
201
+ signedHeaders,
202
+ 'UNSIGNED-PAYLOAD',
203
+ ].join('\n');
204
+ const stringToSign = [
205
+ 'AWS4-HMAC-SHA256',
206
+ amzDate,
207
+ `${credDate}/${region}/s3/aws4_request`,
208
+ sha256Hex(canonicalRequest),
209
+ ].join('\n');
210
+ const expected = computeV4Signature(
211
+ options.secretAccessKey,
212
+ credDate,
213
+ region,
214
+ stringToSign
215
+ );
216
+ if (!timingSafeEqualStr(expected, signature)) {
217
+ const err = signatureMismatch(options, {
218
+ AWSAccessKeyId: accessKeyId,
219
+ StringToSign: stringToSign,
220
+ SignatureProvided: signature,
221
+ });
222
+ if (err) throw err;
223
+ }
224
+ return { mode: 'v4-query', accessKeyId, contentSha256: null };
225
+ }
226
+
227
+ module.exports = { verifyV4Header, verifyV4Query };