fix(portproxy): Fix incorrect import path in test file
This commit is contained in:
parent
38601a41bb
commit
dc3d56771b
@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-02-24 - 3.10.5 - fix(portproxy)
|
||||||
|
Fix incorrect import path in test file
|
||||||
|
|
||||||
|
- Change import path from '../ts/smartproxy.portproxy.js' to '../ts/classes.portproxy.js' in test/test.portproxy.ts
|
||||||
|
|
||||||
## 2025-02-23 - 3.10.4 - fix(PortProxy)
|
## 2025-02-23 - 3.10.4 - fix(PortProxy)
|
||||||
Refactor connection tracking to utilize unified records in PortProxy
|
Refactor connection tracking to utilize unified records in PortProxy
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@push.rocks/tapbundle';
|
||||||
import * as net from 'net';
|
import * as net from 'net';
|
||||||
import { PortProxy } from '../ts/smartproxy.portproxy.js';
|
import { PortProxy } from '../ts/classes.portproxy.js';
|
||||||
|
|
||||||
let testServer: net.Server;
|
let testServer: net.Server;
|
||||||
let portProxy: PortProxy;
|
let portProxy: PortProxy;
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '3.10.4',
|
version: '3.10.5',
|
||||||
description: 'a proxy for handling high workloads of proxying'
|
description: 'a proxy for handling high workloads of proxying'
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import { ProxyRouter } from './smartproxy.classes.router.js';
|
import { ProxyRouter } from './classes.router.js';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
212
ts/classes.port80handler.ts
Normal file
212
ts/classes.port80handler.ts
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import * as http from 'http';
|
||||||
|
import * as acme from 'acme-client';
|
||||||
|
|
||||||
|
interface IDomainCertificate {
|
||||||
|
certObtained: boolean;
|
||||||
|
obtainingInProgress: boolean;
|
||||||
|
certificate?: string;
|
||||||
|
privateKey?: string;
|
||||||
|
challengeToken?: string;
|
||||||
|
challengeKeyAuthorization?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Port80Handler {
|
||||||
|
private domainCertificates: Map<string, IDomainCertificate>;
|
||||||
|
private server: http.Server;
|
||||||
|
private acmeClient: acme.Client | null = null;
|
||||||
|
private accountKey: string | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.domainCertificates = new Map<string, IDomainCertificate>();
|
||||||
|
|
||||||
|
// Create and start an HTTP server on port 80.
|
||||||
|
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
||||||
|
this.server.listen(80, () => {
|
||||||
|
console.log('Port80Handler is listening on port 80');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a domain to be managed.
|
||||||
|
* @param domain The domain to add.
|
||||||
|
*/
|
||||||
|
public addDomain(domain: string): void {
|
||||||
|
if (!this.domainCertificates.has(domain)) {
|
||||||
|
this.domainCertificates.set(domain, { certObtained: false, obtainingInProgress: false });
|
||||||
|
console.log(`Domain added: ${domain}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a domain from management.
|
||||||
|
* @param domain The domain to remove.
|
||||||
|
*/
|
||||||
|
public removeDomain(domain: string): void {
|
||||||
|
if (this.domainCertificates.delete(domain)) {
|
||||||
|
console.log(`Domain removed: ${domain}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy initialization of the ACME client.
|
||||||
|
* Uses Let’s Encrypt’s production directory (for testing you might switch to staging).
|
||||||
|
*/
|
||||||
|
private async getAcmeClient(): Promise<acme.Client> {
|
||||||
|
if (this.acmeClient) {
|
||||||
|
return this.acmeClient;
|
||||||
|
}
|
||||||
|
// Generate a new account key.
|
||||||
|
this.accountKey = await acme.forge.createPrivateKey();
|
||||||
|
this.acmeClient = new acme.Client({
|
||||||
|
directoryUrl: acme.directory.letsencrypt.production, // Use production for a real certificate
|
||||||
|
// For testing, you could use:
|
||||||
|
// directoryUrl: acme.directory.letsencrypt.staging,
|
||||||
|
accountKey: this.accountKey,
|
||||||
|
});
|
||||||
|
// Create a new account. Make sure to update the contact email.
|
||||||
|
await this.acmeClient.createAccount({
|
||||||
|
termsOfServiceAgreed: true,
|
||||||
|
contact: ['mailto:admin@example.com'],
|
||||||
|
});
|
||||||
|
return this.acmeClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles incoming HTTP requests on port 80.
|
||||||
|
* If the request is for an ACME challenge, it responds with the key authorization.
|
||||||
|
* If the domain has a certificate, it redirects to HTTPS; otherwise, it initiates certificate issuance.
|
||||||
|
*/
|
||||||
|
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||||
|
const hostHeader = req.headers.host;
|
||||||
|
if (!hostHeader) {
|
||||||
|
res.statusCode = 400;
|
||||||
|
res.end('Bad Request: Host header is missing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Extract domain (ignoring any port in the Host header)
|
||||||
|
const domain = hostHeader.split(':')[0];
|
||||||
|
|
||||||
|
// If the request is for an ACME HTTP-01 challenge, handle it.
|
||||||
|
if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) {
|
||||||
|
this.handleAcmeChallenge(req, res, domain);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.domainCertificates.has(domain)) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end('Domain not configured');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domainInfo = this.domainCertificates.get(domain)!;
|
||||||
|
|
||||||
|
// If certificate exists, redirect to HTTPS on port 443.
|
||||||
|
if (domainInfo.certObtained) {
|
||||||
|
const redirectUrl = `https://${domain}:443${req.url}`;
|
||||||
|
res.statusCode = 301;
|
||||||
|
res.setHeader('Location', redirectUrl);
|
||||||
|
res.end(`Redirecting to ${redirectUrl}`);
|
||||||
|
} else {
|
||||||
|
// Trigger certificate issuance if not already running.
|
||||||
|
if (!domainInfo.obtainingInProgress) {
|
||||||
|
domainInfo.obtainingInProgress = true;
|
||||||
|
this.obtainCertificate(domain).catch(err => {
|
||||||
|
console.error(`Error obtaining certificate for ${domain}:`, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.statusCode = 503;
|
||||||
|
res.end('Certificate issuance in progress, please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serves the ACME HTTP-01 challenge response.
|
||||||
|
*/
|
||||||
|
private handleAcmeChallenge(req: http.IncomingMessage, res: http.ServerResponse, domain: string): void {
|
||||||
|
const domainInfo = this.domainCertificates.get(domain);
|
||||||
|
if (!domainInfo) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end('Domain not configured');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The token is the last part of the URL.
|
||||||
|
const urlParts = req.url?.split('/');
|
||||||
|
const token = urlParts ? urlParts[urlParts.length - 1] : '';
|
||||||
|
if (domainInfo.challengeToken === token && domainInfo.challengeKeyAuthorization) {
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
|
res.end(domainInfo.challengeKeyAuthorization);
|
||||||
|
console.log(`Served ACME challenge response for ${domain}`);
|
||||||
|
} else {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end('Challenge token not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses acme-client to perform a full ACME HTTP-01 challenge to obtain a certificate.
|
||||||
|
* On success, it stores the certificate and key in memory and clears challenge data.
|
||||||
|
*/
|
||||||
|
private async obtainCertificate(domain: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const client = await this.getAcmeClient();
|
||||||
|
|
||||||
|
// Create a new order for the domain.
|
||||||
|
const order = await client.createOrder({
|
||||||
|
identifiers: [{ type: 'dns', value: domain }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the authorizations for the order.
|
||||||
|
const authorizations = await client.getAuthorizations(order);
|
||||||
|
for (const authz of authorizations) {
|
||||||
|
const challenge = authz.challenges.find(ch => ch.type === 'http-01');
|
||||||
|
if (!challenge) {
|
||||||
|
throw new Error('HTTP-01 challenge not found');
|
||||||
|
}
|
||||||
|
// Get the key authorization for the challenge.
|
||||||
|
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
||||||
|
const domainInfo = this.domainCertificates.get(domain)!;
|
||||||
|
domainInfo.challengeToken = challenge.token;
|
||||||
|
domainInfo.challengeKeyAuthorization = keyAuthorization;
|
||||||
|
|
||||||
|
// Notify the ACME server that the challenge is ready.
|
||||||
|
await client.verifyChallenge(authz, challenge, keyAuthorization);
|
||||||
|
await client.completeChallenge(challenge);
|
||||||
|
// Wait until the challenge is validated.
|
||||||
|
await client.waitForValidStatus(challenge);
|
||||||
|
console.log(`HTTP-01 challenge completed for ${domain}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a CSR and a new private key for the domain.
|
||||||
|
const [csr, privateKey] = await acme.forge.createCsr({
|
||||||
|
commonName: domain,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Finalize the order and obtain the certificate.
|
||||||
|
await client.finalizeOrder(order, csr);
|
||||||
|
const certificate = await client.getCertificate(order);
|
||||||
|
|
||||||
|
const domainInfo = this.domainCertificates.get(domain)!;
|
||||||
|
domainInfo.certificate = certificate;
|
||||||
|
domainInfo.privateKey = privateKey;
|
||||||
|
domainInfo.certObtained = true;
|
||||||
|
domainInfo.obtainingInProgress = false;
|
||||||
|
delete domainInfo.challengeToken;
|
||||||
|
delete domainInfo.challengeKeyAuthorization;
|
||||||
|
|
||||||
|
console.log(`Certificate obtained for ${domain}`);
|
||||||
|
// In a real application, you would persist the certificate and key,
|
||||||
|
// then reload your TLS server with the new credentials.
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error during certificate issuance for ${domain}:`, error);
|
||||||
|
const domainInfo = this.domainCertificates.get(domain);
|
||||||
|
if (domainInfo) {
|
||||||
|
domainInfo.obtainingInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage:
|
||||||
|
// const handler = new Port80Handler();
|
||||||
|
// handler.addDomain('example.com');
|
@ -1,3 +1,3 @@
|
|||||||
export * from './smartproxy.classes.networkproxy.js';
|
export * from './classes.networkproxy.js';
|
||||||
export * from './smartproxy.portproxy.js';
|
export * from './classes.portproxy.js';
|
||||||
export * from './smartproxy.classes.sslredirect.js';
|
export * from './classes.sslredirect.js';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user