fix(certificates): Update static-route certificate metadata for ACME challenges
This commit is contained in:
parent
02e77655ad
commit
4134d2842c
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"expiryDate": "2025-08-16T18:25:31.732Z",
|
"expiryDate": "2025-08-17T12:04:34.427Z",
|
||||||
"issueDate": "2025-05-18T18:25:31.732Z",
|
"issueDate": "2025-05-19T12:04:34.427Z",
|
||||||
"savedAt": "2025-05-18T18:25:31.734Z"
|
"savedAt": "2025-05-19T12:04:34.429Z"
|
||||||
}
|
}
|
@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# 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)
|
## 2025-05-19 - 19.3.0 - feat(smartproxy)
|
||||||
Update dependencies and enhance ACME certificate provisioning with wildcard support
|
Update dependencies and enhance ACME certificate provisioning with wildcard support
|
||||||
|
|
||||||
|
129
test/test.acme-http-challenge.ts
Normal file
129
test/test.acme-http-challenge.ts
Normal file
@ -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();
|
139
test/test.acme-route-creation.ts
Normal file
139
test/test.acme-route-creation.ts
Normal file
@ -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<void>((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();
|
116
test/test.acme-simple.ts
Normal file
116
test/test.acme-simple.ts
Normal file
@ -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<void>((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<void>((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();
|
104
test/test.simple-acme-mock.ts
Normal file
104
test/test.simple-acme-mock.ts
Normal file
@ -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();
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
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.'
|
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.'
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user