diff --git a/core/s3.js b/core/s3.js index 8acaeda..2153ab0 100644 --- a/core/s3.js +++ b/core/s3.js @@ -14,9 +14,11 @@ const hmac = (k, s) => crypto.createHmac('sha256', k).update(s).digest(); // RFC 3986 encoding as SigV4 wants it; '/' kept for object keys in paths. function uriEncode(str, keepSlash) { - return encodeURIComponent(str) - .replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase()) - .replace(keepSlash ? /%2F/g : /$^/, '/'); + const out = encodeURIComponent(str) + .replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase()); + // (A "never matches" regex like /$^/ is not one: it matches the empty + // string, which turned every empty query value into "/".) + return keepSlash ? out.replace(/%2F/g, '/') : out; } function canonicalQuery(query) { diff --git a/package.json b/package.json index 9ebe0eb..abef19e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pithos", - "version": "0.3.2", + "version": "0.3.3", "description": "A control panel for s3d, the Sia S3 gateway: daemon, users and keys, buckets, upload pipeline.", "license": "MPL-2.0", "type": "module", diff --git a/test/sigv4.test.js b/test/sigv4.test.js index cc4f88b..cf99345 100644 --- a/test/sigv4.test.js +++ b/test/sigv4.test.js @@ -77,3 +77,20 @@ test('presigned URL matches aws cli', { skip: !hasAws && 'aws cli not installed' 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=(&|$)/); +}); diff --git a/theseus/addon.json b/theseus/addon.json index 34c9504..8ea5cc9 100644 --- a/theseus/addon.json +++ b/theseus/addon.json @@ -1,7 +1,7 @@ { "id": "pithos", "name": "Pithos", - "version": "0.3.2", + "version": "0.3.3", "description": "Run s3d, the Sia S3 gateway, from the Theseus sidebar: connect it to a Sia indexer, create S3 users and access keys, browse and share buckets, and watch uploads reach Sia.", "author": "Silent Mode", "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",