From 4134d2842c689b7a5357b78156cd9245d5edd54a Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Mon, 19 May 2025 12:17:21 +0000 Subject: [PATCH] fix(certificates): Update static-route certificate metadata for ACME challenges --- certs/static-route/meta.json | 6 +- changelog.md | 5 ++ test/test.acme-http-challenge.ts | 129 ++++++++++++++++++++++++++++ test/test.acme-route-creation.ts | 139 +++++++++++++++++++++++++++++++ test/test.acme-simple.ts | 116 ++++++++++++++++++++++++++ test/test.simple-acme-mock.ts | 104 +++++++++++++++++++++++ ts/00_commitinfo_data.ts | 2 +- 7 files changed, 497 insertions(+), 4 deletions(-) create mode 100644 test/test.acme-http-challenge.ts create mode 100644 test/test.acme-route-creation.ts create mode 100644 test/test.acme-simple.ts create mode 100644 test/test.simple-acme-mock.ts diff --git a/certs/static-route/meta.json b/certs/static-route/meta.json index e7ef77a..6008b60 100644 --- a/certs/static-route/meta.json +++ b/certs/static-route/meta.json @@ -1,5 +1,5 @@ { - "expiryDate": "2025-08-16T18:25:31.732Z", - "issueDate": "2025-05-18T18:25:31.732Z", - "savedAt": "2025-05-18T18:25:31.734Z" + "expiryDate": "2025-08-17T12:04:34.427Z", + "issueDate": "2025-05-19T12:04:34.427Z", + "savedAt": "2025-05-19T12:04:34.429Z" } \ No newline at end of file diff --git a/changelog.md b/changelog.md index c4432db..60f214e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,10 @@ # Changelog +## 2025-05-19 - 19.3.1 - fix(certificates) +Update static-route certificate metadata for ACME challenges + +- Updated expiryDate and issueDate in certs/static-route/meta.json to reflect new certificate issuance information + ## 2025-05-19 - 19.3.0 - feat(smartproxy) Update dependencies and enhance ACME certificate provisioning with wildcard support diff --git a/test/test.acme-http-challenge.ts b/test/test.acme-http-challenge.ts new file mode 100644 index 0000000..6c8b486 --- /dev/null +++ b/test/test.acme-http-challenge.ts @@ -0,0 +1,129 @@ +import { tap, expect } from '@git.zone/tstest/tapbundle'; +import * as plugins from '../ts/plugins.js'; +import { SmartProxy } from '../ts/index.js'; + +tap.test('should handle HTTP requests on port 80 for ACME challenges', async (tools) => { + tools.timeout(10000); + + // Track HTTP requests that are handled + const handledRequests: any[] = []; + + const settings = { + routes: [ + { + name: 'acme-test-route', + match: { + ports: [18080], // Use high port to avoid permission issues + path: '/.well-known/acme-challenge/*' + }, + action: { + type: 'static' as const, + handler: async (context) => { + handledRequests.push({ + path: context.path, + method: context.method, + headers: context.headers + }); + + // Simulate ACME challenge response + const token = context.path?.split('/').pop() || ''; + return { + status: 200, + headers: { 'Content-Type': 'text/plain' }, + body: `challenge-response-for-${token}` + }; + } + } + } + ] + }; + + const proxy = new SmartProxy(settings); + + // Mock NFTables manager + (proxy as any).nftablesManager = { + ensureNFTablesSetup: async () => {}, + stop: async () => {} + }; + + await proxy.start(); + + // Make an HTTP request to the challenge endpoint + const response = await fetch('http://localhost:18080/.well-known/acme-challenge/test-token', { + method: 'GET' + }); + + // Verify response + expect(response.status).toEqual(200); + const body = await response.text(); + expect(body).toEqual('challenge-response-for-test-token'); + + // Verify request was handled + expect(handledRequests.length).toEqual(1); + expect(handledRequests[0].path).toEqual('/.well-known/acme-challenge/test-token'); + expect(handledRequests[0].method).toEqual('GET'); + + await proxy.stop(); +}); + +tap.test('should parse HTTP headers correctly', async (tools) => { + tools.timeout(10000); + + const capturedContext: any = {}; + + const settings = { + routes: [ + { + name: 'header-test-route', + match: { + ports: [18081] + }, + action: { + type: 'static' as const, + handler: async (context) => { + Object.assign(capturedContext, context); + return { + status: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + received: context.headers + }) + }; + } + } + } + ] + }; + + const proxy = new SmartProxy(settings); + + // Mock NFTables manager + (proxy as any).nftablesManager = { + ensureNFTablesSetup: async () => {}, + stop: async () => {} + }; + + await proxy.start(); + + // Make request with custom headers + const response = await fetch('http://localhost:18081/test', { + method: 'POST', + headers: { + 'X-Custom-Header': 'test-value', + 'User-Agent': 'test-agent' + } + }); + + expect(response.status).toEqual(200); + const body = await response.json(); + + // Verify headers were parsed correctly + expect(capturedContext.headers['x-custom-header']).toEqual('test-value'); + expect(capturedContext.headers['user-agent']).toEqual('test-agent'); + expect(capturedContext.method).toEqual('POST'); + expect(capturedContext.path).toEqual('/test'); + + await proxy.stop(); +}); + +tap.start(); \ No newline at end of file diff --git a/test/test.acme-route-creation.ts b/test/test.acme-route-creation.ts new file mode 100644 index 0000000..b0a877c --- /dev/null +++ b/test/test.acme-route-creation.ts @@ -0,0 +1,139 @@ +import { tap, expect } from '@git.zone/tstest/tapbundle'; +import { SmartProxy } from '../ts/index.js'; + +/** + * Test that verifies ACME challenge routes are properly created + */ +tap.test('should create ACME challenge route with high ports', async (tools) => { + tools.timeout(5000); + + const capturedRoutes: any[] = []; + + const settings = { + routes: [ + { + name: 'secure-route', + match: { + ports: [18443], // High port to avoid permission issues + domains: 'test.local' + }, + action: { + type: 'forward' as const, + target: { host: 'localhost', port: 8080 }, + tls: { + mode: 'terminate' as const, + certificate: 'auto' + } + } + } + ], + acme: { + email: 'test@test.local', + port: 18080 // High port for ACME challenges + } + }; + + const proxy = new SmartProxy(settings); + + // Capture route updates + const originalUpdateRoutes = (proxy as any).updateRoutesInternal.bind(proxy); + (proxy as any).updateRoutesInternal = async function(routes: any[]) { + capturedRoutes.push([...routes]); + return originalUpdateRoutes(routes); + }; + + await proxy.start(); + + // Check that ACME challenge route was added + const finalRoutes = capturedRoutes[capturedRoutes.length - 1]; + const challengeRoute = finalRoutes.find((r: any) => r.name === 'acme-challenge'); + + expect(challengeRoute).toBeDefined(); + expect(challengeRoute.match.path).toEqual('/.well-known/acme-challenge/*'); + expect(challengeRoute.match.ports).toEqual(18080); + expect(challengeRoute.action.type).toEqual('static'); + expect(challengeRoute.priority).toEqual(1000); + + await proxy.stop(); +}); + +tap.test('should handle HTTP request parsing correctly', async (tools) => { + tools.timeout(5000); + + let handlerCalled = false; + let receivedContext: any; + + const settings = { + routes: [ + { + name: 'test-static', + match: { + ports: [18090], + path: '/test/*' + }, + action: { + type: 'static' as const, + handler: async (context) => { + handlerCalled = true; + receivedContext = context; + return { + status: 200, + headers: { 'Content-Type': 'text/plain' }, + body: 'OK' + }; + } + } + } + ] + }; + + const proxy = new SmartProxy(settings); + + // Mock NFTables manager + (proxy as any).nftablesManager = { + ensureNFTablesSetup: async () => {}, + stop: async () => {} + }; + + await proxy.start(); + + // Create a simple HTTP request + const client = new plugins.net.Socket(); + + await new Promise((resolve, reject) => { + client.connect(18090, 'localhost', () => { + // Send HTTP request + const request = [ + 'GET /test/example HTTP/1.1', + 'Host: localhost:18090', + 'User-Agent: test-client', + '', + '' + ].join('\r\n'); + + client.write(request); + + // Wait for response + client.on('data', (data) => { + const response = data.toString(); + expect(response).toContain('HTTP/1.1 200'); + expect(response).toContain('OK'); + client.end(); + resolve(); + }); + }); + + client.on('error', reject); + }); + + // Verify handler was called + expect(handlerCalled).toBeTrue(); + expect(receivedContext).toBeDefined(); + expect(receivedContext.path).toEqual('/test/example'); + expect(receivedContext.method).toEqual('GET'); + expect(receivedContext.headers.host).toEqual('localhost:18090'); + + await proxy.stop(); +}); + +tap.start(); \ No newline at end of file diff --git a/test/test.acme-simple.ts b/test/test.acme-simple.ts new file mode 100644 index 0000000..2200d5c --- /dev/null +++ b/test/test.acme-simple.ts @@ -0,0 +1,116 @@ +import { tap, expect } from '@git.zone/tstest/tapbundle'; +import * as net from 'net'; + +/** + * Simple test to verify HTTP parsing works for ACME challenges + */ +tap.test('should parse HTTP requests correctly', async (tools) => { + tools.timeout(15000); + + let receivedRequest = ''; + + // Create a simple HTTP server to test the parsing + const server = net.createServer((socket) => { + socket.on('data', (data) => { + receivedRequest = data.toString(); + + // Send response + const response = [ + 'HTTP/1.1 200 OK', + 'Content-Type: text/plain', + 'Content-Length: 2', + '', + 'OK' + ].join('\r\n'); + + socket.write(response); + socket.end(); + }); + }); + + await new Promise((resolve) => { + server.listen(18091, () => { + console.log('Test server listening on port 18091'); + resolve(); + }); + }); + + // Connect and send request + const client = net.connect(18091, 'localhost'); + + await new Promise((resolve, reject) => { + client.on('connect', () => { + const request = [ + 'GET /.well-known/acme-challenge/test-token HTTP/1.1', + 'Host: localhost:18091', + 'User-Agent: test-client', + '', + '' + ].join('\r\n'); + + client.write(request); + }); + + client.on('data', (data) => { + const response = data.toString(); + expect(response).toContain('200 OK'); + client.end(); + }); + + client.on('end', () => { + resolve(); + }); + + client.on('error', reject); + }); + + // Verify we received the request + expect(receivedRequest).toContain('GET /.well-known/acme-challenge/test-token'); + expect(receivedRequest).toContain('Host: localhost:18091'); + + server.close(); +}); + +/** + * Test to verify ACME route configuration + */ +tap.test('should configure ACME challenge route', async () => { + // Simple test to verify the route configuration structure + const challengeRoute = { + name: 'acme-challenge', + priority: 1000, + match: { + ports: 80, + path: '/.well-known/acme-challenge/*' + }, + action: { + type: 'static', + handler: async (context: any) => { + const token = context.path?.split('/').pop() || ''; + return { + status: 200, + headers: { 'Content-Type': 'text/plain' }, + body: `challenge-response-${token}` + }; + } + } + }; + + expect(challengeRoute.name).toEqual('acme-challenge'); + expect(challengeRoute.match.path).toEqual('/.well-known/acme-challenge/*'); + expect(challengeRoute.match.ports).toEqual(80); + expect(challengeRoute.priority).toEqual(1000); + + // Test the handler + const context = { + path: '/.well-known/acme-challenge/test-token', + method: 'GET', + headers: {} + }; + + const response = await challengeRoute.action.handler(context); + expect(response.status).toEqual(200); + expect(response.body).toEqual('challenge-response-test-token'); +}); + +tap.start(); \ No newline at end of file diff --git a/test/test.simple-acme-mock.ts b/test/test.simple-acme-mock.ts new file mode 100644 index 0000000..dd15c16 --- /dev/null +++ b/test/test.simple-acme-mock.ts @@ -0,0 +1,104 @@ +import { tap, expect } from '@git.zone/tstest/tapbundle'; +import { SmartProxy } from '../ts/index.js'; + +/** + * Simple test to check that ACME challenge routes are created + */ +tap.test('should create ACME challenge route', async (tools) => { + tools.timeout(5000); + + const mockRouteUpdates: any[] = []; + + const settings = { + routes: [ + { + name: 'secure-route', + match: { + ports: [443], + domains: 'test.example.com' + }, + action: { + type: 'forward' as const, + target: { host: 'localhost', port: 8080 }, + tls: { + mode: 'terminate' as const, + certificate: 'auto', + acme: { + email: 'test@test.local' // Use non-example.com domain + } + } + } + } + ] + }; + + const proxy = new SmartProxy(settings); + + // Mock certificate manager + let updateRoutesCallback: any; + + (proxy as any).createCertificateManager = async function(routes: any[], certDir: string, acmeOptions: any) { + const mockCertManager = { + setUpdateRoutesCallback: function(callback: any) { + updateRoutesCallback = callback; + }, + setNetworkProxy: function() {}, + setGlobalAcmeDefaults: function() {}, + setAcmeStateManager: function() {}, + initialize: async function() { + // Simulate adding ACME challenge route + if (updateRoutesCallback) { + const challengeRoute = { + name: 'acme-challenge', + priority: 1000, + match: { + ports: 80, + path: '/.well-known/acme-challenge/*' + }, + action: { + type: 'static', + handler: async (context: any) => { + const token = context.path?.split('/').pop() || ''; + return { + status: 200, + headers: { 'Content-Type': 'text/plain' }, + body: `mock-challenge-response-${token}` + }; + } + } + }; + + const updatedRoutes = [...routes, challengeRoute]; + mockRouteUpdates.push(updatedRoutes); + await updateRoutesCallback(updatedRoutes); + } + }, + getAcmeOptions: () => acmeOptions, + getState: () => ({ challengeRouteActive: false }), + stop: async () => {} + }; + return mockCertManager; + }; + + // Mock NFTables + (proxy as any).nftablesManager = { + ensureNFTablesSetup: async () => {}, + stop: async () => {} + }; + + await proxy.start(); + + // Verify that routes were updated with challenge route + expect(mockRouteUpdates.length).toBeGreaterThan(0); + + const lastUpdate = mockRouteUpdates[mockRouteUpdates.length - 1]; + const challengeRoute = lastUpdate.find((r: any) => r.name === 'acme-challenge'); + + expect(challengeRoute).toBeDefined(); + expect(challengeRoute.match.path).toEqual('/.well-known/acme-challenge/*'); + expect(challengeRoute.match.ports).toEqual(80); + + await proxy.stop(); +}); + +tap.start(); \ No newline at end of file diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index b031413..816934a 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '19.3.0', + version: '19.3.1', description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.' }