From 04ddec50ade46029b31b310d55fb584a8a53cbd0 Mon Sep 17 00:00:00 2001 From: Mark Rapson Date: Sat, 25 Jul 2026 17:58:00 +0100 Subject: [PATCH] Release 1.5.7 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- src/request-auth.js | 51 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd4a625..73bc266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ All notable changes to this project will be documented in this file. - No unreleased changes recorded yet. +## 1.5.7 - 2026-07-25 + +### Fixed + +- Upload removal requests now authenticate correctly when a deployed player parses a bodyless `DELETE` request as an empty object, which could cause 401s for newly uploaded images. + ## 1.5.6 - 2026-07-25 ### Changed diff --git a/package.json b/package.json index 52c418d..e9a2bbe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "1.5.6", + "version": "1.5.7", "private": false, "description": "Pulse Signage application with MySQL and media storage", "repository": { diff --git a/src/request-auth.js b/src/request-auth.js index 00749fb..ac266b3 100644 --- a/src/request-auth.js +++ b/src/request-auth.js @@ -47,6 +47,19 @@ function canonicalize(value) { return value === undefined ? undefined : value; } +function normalizeRequestPath(pathValue) { + const value = String(pathValue || '').trim(); + if (!value) { + return ''; + } + + try { + return decodeURIComponent(value); + } catch (_error) { + return value; + } +} + function stableJson(value) { return JSON.stringify(canonicalize(value)); } @@ -56,10 +69,38 @@ function hashPayload(value) { return crypto.createHash('sha256').update(normalized).digest('hex'); } +function normalizeRequestAuthBody(req) { + if (!req) { + return null; + } + + const body = req.body; + if (body === undefined || body === null) { + return null; + } + + if (Buffer.isBuffer(body)) { + return body; + } + + if (typeof body === 'object' && !Array.isArray(body)) { + const hasFields = Object.keys(body).length > 0; + if (!hasFields) { + const contentLength = Number(String(req.headers && req.headers['content-length'] || '').trim() || 0); + const contentType = String(req.headers && req.headers['content-type'] || '').trim(); + if (!contentLength || !contentType) { + return null; + } + } + } + + return body; +} + function getRequestPath(req) { const explicitPath = String(req && req.path ? req.path : '').trim(); if (explicitPath) { - return explicitPath; + return normalizeRequestPath(explicitPath); } const rawUrl = String(req && req.url ? req.url : '').trim(); @@ -68,9 +109,9 @@ function getRequestPath(req) { } try { - return new URL(rawUrl, 'http://localhost').pathname; + return normalizeRequestPath(new URL(rawUrl, 'http://localhost').pathname); } catch (_error) { - return rawUrl.split('?')[0] || ''; + return normalizeRequestPath(rawUrl.split('?')[0] || ''); } } @@ -167,7 +208,7 @@ function createRequestAuthHeaders(options) { } const method = String(options && options.method || 'GET').trim().toUpperCase(); - const pathname = String(options && options.pathname || '').trim(); + const pathname = normalizeRequestPath(options && options.pathname || ''); const timestamp = String(options && options.timestamp || Date.now()).trim(); const bodyDigest = hashPayload(options && Object.prototype.hasOwnProperty.call(options, 'body') ? options.body : null); const signature = signText(secret, `request\n${method}\n${pathname}\n${timestamp}\n${bodyDigest}`); @@ -199,7 +240,7 @@ function verifyRequestAuth(req) { return false; } - const expectedSignature = signText(secret, `request\n${String(req.method || 'GET').trim().toUpperCase()}\n${getRequestPath(req)}\n${timestamp}\n${hashPayload(req.body)}`); + const expectedSignature = signText(secret, `request\n${String(req.method || 'GET').trim().toUpperCase()}\n${getRequestPath(req)}\n${timestamp}\n${hashPayload(normalizeRequestAuthBody(req))}`); return timingSafeEqualHex(expectedSignature, signature); }