diff --git a/CHANGELOG.md b/CHANGELOG.md index d9a3b1b..95459eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +## [3.1.2] - 2025-12-07 + +### 🐛 Bug Fixes +- **Fixed certificate download endpoints**: Corrected path resolution for certificate, key, and bundle downloads + - Fixed `validateAndSanitizePath` usage to properly extract resolved path from returned object + - Fixed `validateFilename` error handling to catch thrown exceptions instead of checking return value + - Added support for both `.pem`/`-key.pem` and `.crt`/`.key` certificate formats in bundle downloads + - Resolved 404 errors when attempting to download certificates + +## [3.1.1] - 2025-12-05 + +### 🐛 Bug Fixes +- **Fixed file upload endpoint**: Mounted missing file routes module to enable certificate uploads + - Added `createFileRoutes` import and mounting in server.js + - Corrected upload API endpoint from `/api/certificates/upload` to `/api/upload` +- **Fixed download URL paths**: Removed duplicate `/api` prefix in frontend download URLs + - Fixed downloadCert, downloadKey, downloadBundle, and downloadRootCA functions + - API_BASE constant already includes `/api`, preventing `/api/api/...` double prefix + ## [3.1.0] - 2025-10-09 ### ✨ New Features - Web-Based Settings Management diff --git a/docker-build-push.sh b/docker-build-push.sh index ba71b87..a94552a 100755 --- a/docker-build-push.sh +++ b/docker-build-push.sh @@ -7,7 +7,7 @@ set -e # Configuration IMAGE_NAME="jeffcaldwellca/mkcertweb" -VERSION="3.1.1" +VERSION="3.1.2" # Colors for output GREEN='\033[0;32m' diff --git a/docker-compose.yml b/docker-compose.yml index 05bbb9b..a50ed15 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: mkcert-web-ui: - image: jeffcaldwellca/mkcertweb:3.1.1 + image: jeffcaldwellca/mkcertweb:3.1.2 ports: - "3000:3000" # HTTP port - "3443:3443" # HTTPS port diff --git a/public/script.js b/public/script.js index 3f436d7..8445fc1 100644 --- a/public/script.js +++ b/public/script.js @@ -1415,22 +1415,22 @@ async function downloadFile(url, filename) { } function downloadCert(folderParam, filename) { - const url = API_BASE + '/api/download/cert/' + folderParam + '/' + filename; + const url = API_BASE + '/download/cert/' + folderParam + '/' + filename; downloadFile(url, filename); } function downloadKey(folderParam, filename) { - const url = API_BASE + '/api/download/key/' + folderParam + '/' + filename; + const url = API_BASE + '/download/key/' + folderParam + '/' + filename; downloadFile(url, filename); } function downloadBundle(folderParam, certname) { - const url = API_BASE + '/api/download/bundle/' + folderParam + '/' + certname; + const url = API_BASE + '/download/bundle/' + folderParam + '/' + certname; downloadFile(url, certname + '.zip'); } function downloadRootCA() { - const url = API_BASE + '/api/download/rootca'; + const url = API_BASE + '/download/rootca'; downloadFile(url, 'mkcert-rootCA.pem'); } diff --git a/src/routes/certificates.js b/src/routes/certificates.js index bdba22d..0259146 100644 --- a/src/routes/certificates.js +++ b/src/routes/certificates.js @@ -605,7 +605,9 @@ const createCertificateRoutes = (config, rateLimiters, requireAuth) => { const certificatesDir = path.join(process.cwd(), 'certificates'); // Validate the filename through security module - if (!security.validateFilename(filename)) { + try { + security.validateFilename(filename); + } catch (error) { return apiResponse.badRequest(res, 'Invalid filename'); } @@ -622,7 +624,8 @@ const createCertificateRoutes = (config, rateLimiters, requireAuth) => { // Use security-validated path let filePath; try { - filePath = security.validateAndSanitizePath(filename, sourceDir); + const pathInfo = security.validateAndSanitizePath(filename, sourceDir); + filePath = pathInfo.resolved; } catch (error) { return apiResponse.badRequest(res, 'Invalid file path'); } @@ -646,7 +649,9 @@ const createCertificateRoutes = (config, rateLimiters, requireAuth) => { const certificatesDir = path.join(process.cwd(), 'certificates'); // Validate the filename through security module - if (!security.validateFilename(filename)) { + try { + security.validateFilename(filename); + } catch (error) { return apiResponse.badRequest(res, 'Invalid filename'); } @@ -663,7 +668,8 @@ const createCertificateRoutes = (config, rateLimiters, requireAuth) => { // Use security-validated path let filePath; try { - filePath = security.validateAndSanitizePath(filename, sourceDir); + const pathInfo = security.validateAndSanitizePath(filename, sourceDir); + filePath = pathInfo.resolved; } catch (error) { return apiResponse.badRequest(res, 'Invalid file path'); } @@ -687,7 +693,9 @@ const createCertificateRoutes = (config, rateLimiters, requireAuth) => { const certificatesDir = path.join(process.cwd(), 'certificates'); // Validate the certificate name through security module - if (!security.validateFilename(`${certname}.pem`)) { + try { + security.validateFilename(`${certname}.pem`); + } catch (error) { return apiResponse.badRequest(res, 'Invalid certificate name'); } @@ -701,20 +709,46 @@ const createCertificateRoutes = (config, rateLimiters, requireAuth) => { return apiResponse.badRequest(res, 'Invalid folder parameter'); } - // Use security-validated paths - let certFile, keyFile; + // Use security-validated paths - try both .pem and .crt formats + let certFile, keyFile, certExt, keyExt; + const fs = require('fs'); + try { - certFile = security.validateAndSanitizePath(`${certname}.pem`, sourceDir); - keyFile = security.validateAndSanitizePath(`${certname}-key.pem`, sourceDir); + // Try .pem format for certificate + let certPathInfo = security.validateAndSanitizePath(`${certname}.pem`, sourceDir); + if (fs.existsSync(certPathInfo.resolved)) { + certFile = certPathInfo.resolved; + certExt = '.pem'; + } else { + // Try .crt format + certPathInfo = security.validateAndSanitizePath(`${certname}.crt`, sourceDir); + if (fs.existsSync(certPathInfo.resolved)) { + certFile = certPathInfo.resolved; + certExt = '.crt'; + } + } + + // Try -key.pem format for key + let keyPathInfo = security.validateAndSanitizePath(`${certname}-key.pem`, sourceDir); + if (fs.existsSync(keyPathInfo.resolved)) { + keyFile = keyPathInfo.resolved; + keyExt = '-key.pem'; + } else { + // Try .key format + keyPathInfo = security.validateAndSanitizePath(`${certname}.key`, sourceDir); + if (fs.existsSync(keyPathInfo.resolved)) { + keyFile = keyPathInfo.resolved; + keyExt = '.key'; + } + } } catch (error) { return apiResponse.badRequest(res, 'Invalid file path'); } try { - const fs = require('fs'); const archiver = require('archiver'); - if (!fs.existsSync(certFile) && !fs.existsSync(keyFile)) { + if (!certFile && !keyFile) { return apiResponse.notFound(res, 'Certificate files not found'); } @@ -724,11 +758,11 @@ const createCertificateRoutes = (config, rateLimiters, requireAuth) => { const archive = archiver('zip', { zlib: { level: 9 }}); archive.pipe(res); - if (fs.existsSync(certFile)) { - archive.file(certFile, { name: `${certname}.pem` }); + if (certFile) { + archive.file(certFile, { name: `${certname}${certExt}` }); } - if (fs.existsSync(keyFile)) { - archive.file(keyFile, { name: `${certname}-key.pem` }); + if (keyFile) { + archive.file(keyFile, { name: `${certname}${keyExt}` }); } await archive.finalize();