// Cross-checks the SigV4 signer against the real AWS CLI: a capture server // records what `aws` sends, and we re-sign the same request ourselves. Skips // when the aws CLI is not installed. import test from 'node:test'; import assert from 'node:assert/strict'; import http from 'node:http'; import { execFileSync, execFile } from 'node:child_process'; import { S3Client, objectPath } from '../core/s3.js'; const AK = 'AKIDEXAMPLE12345678'; const SK = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'; let hasAws = true; try { execFileSync('aws', ['--version'], { stdio: 'ignore' }); } catch { hasAws = false; } function capture(fn) { return new Promise((resolve, reject) => { const server = http.createServer((req, res) => { const chunks = []; req.on('data', (c) => chunks.push(c)); req.on('end', () => { res.writeHead(200, { 'content-type': 'application/xml' }); res.end('false'); server.close(); resolve({ method: req.method, url: req.url, headers: req.headers }); }); }); server.listen(0, '127.0.0.1', () => fn(server.address().port).catch(reject)); }); } function runAws(args) { return new Promise((resolve) => execFile('aws', args, { env: { ...process.env, AWS_ACCESS_KEY_ID: AK, AWS_SECRET_ACCESS_KEY: SK, AWS_REGION: 'us-east-1', AWS_CONFIG_FILE: 'nul', AWS_SHARED_CREDENTIALS_FILE: 'nul' }, }, () => resolve())); } function resign(port, req, bucket, key) { const client = new S3Client({ endpoint: `http://127.0.0.1:${port}`, accessKeyId: AK, secretKey: SK }); const auth = req.headers.authorization; const signedNames = auth.match(/SignedHeaders=([^,]+)/)[1].split(';'); const headers = {}; for (const n of signedNames) if (!['host', 'x-amz-date', 'x-amz-content-sha256'].includes(n)) headers[n] = req.headers[n]; const u = new URL(req.url, 'http://x'); const query = {}; for (const [k, v] of u.searchParams) query[k] = v; const ours = client.sign(req.method, objectPath(bucket, key), query, headers, { now: req.headers['x-amz-date'], payloadHash: req.headers['x-amz-content-sha256'], }); return { theirs: auth.match(/Signature=(\w+)/)[1], ours: ours.authorization.match(/Signature=(\w+)/)[1] }; } test('list with awkward prefix matches aws cli', { skip: !hasAws && 'aws cli not installed' }, async () => { let port; const req = await capture((p) => { port = p; return runAws(['s3api', 'list-objects-v2', '--bucket', 'my-bucket', '--prefix', "a b/ü+x=(1)*'~", '--endpoint-url', `http://127.0.0.1:${p}`]); }); const { theirs, ours } = resign(port, req, 'my-bucket'); assert.equal(ours, theirs); }); test('object key path encoding matches aws cli', { skip: !hasAws && 'aws cli not installed' }, async () => { let port; const key = "dir/a b+c(1)!ü*'~.txt"; const req = await capture((p) => { port = p; return runAws(['s3api', 'delete-object', '--bucket', 'my-bucket', '--key', key, '--endpoint-url', `http://127.0.0.1:${p}`]); }); const { theirs, ours } = resign(port, req, 'my-bucket', key); assert.equal(ours, theirs); }); test('presigned URL matches aws cli', { skip: !hasAws && 'aws cli not installed' }, () => { const key = 'dir/report 2026 (final).pdf'; const theirs = execFileSync('aws', ['s3', 'presign', `s3://my-bucket/${key}`, '--expires-in', '3600', '--endpoint-url', 'http://127.0.0.1:18000'], { env: { ...process.env, AWS_ACCESS_KEY_ID: AK, AWS_SECRET_ACCESS_KEY: SK, AWS_REGION: 'us-east-1', AWS_CONFIG_FILE: 'nul', AWS_SHARED_CREDENTIALS_FILE: 'nul' }, }).toString().trim(); const t = new URL(theirs); const client = new S3Client({ endpoint: 'http://127.0.0.1:18000', accessKeyId: AK, secretKey: SK }); const ours = new URL(client.presign('my-bucket', key, 3600, undefined, t.searchParams.get('X-Amz-Date'))); assert.equal(ours.pathname, t.pathname); assert.equal(ours.searchParams.get('X-Amz-Signature'), t.searchParams.get('X-Amz-Signature')); }); // Regression: empty query values (prefix=, policy) were sent as "/" and // every bucket listing failed with SignatureDoesNotMatch against s3d. test('empty query values stay empty and match the aws cli', { skip: !hasAws && 'aws cli not installed' }, async () => { let port; const req = await capture((p) => { port = p; return runAws(['s3api', 'list-objects-v2', '--bucket', 'my-bucket', '--prefix', '', '--delimiter', '/', '--endpoint-url', `http://127.0.0.1:${p}`]); }); const { theirs, ours } = resign(port, req, 'my-bucket'); assert.equal(ours, theirs); }); test('client sends empty values as empty', async () => { const seen = await new Promise((resolve) => { const server = http.createServer((req, res) => { res.end('false'); server.close(); resolve(req.url); }); server.listen(0, '127.0.0.1', () => new S3Client({ endpoint: `http://127.0.0.1:${server.address().port}`, accessKeyId: AK, secretKey: SK }).listObjects('b').catch(() => {})); }); assert.match(seen, /[?&]prefix=(&|$)/); });