@push.rocks/smartproxy 7.1.2 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/networkproxy/classes.np.networkproxy.d.ts +1 -0
- package/dist_ts/networkproxy/classes.np.networkproxy.js +5 -1
- package/dist_ts/plugins.d.ts +5 -3
- package/dist_ts/plugins.js +6 -4
- package/dist_ts/port80handler/classes.port80handler.d.ts +6 -20
- package/dist_ts/port80handler/classes.port80handler.js +64 -155
- package/dist_ts/smartproxy/classes.pp.interfaces.d.ts +9 -0
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.d.ts +5 -1
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.js +11 -1
- package/dist_ts/smartproxy/classes.smartproxy.d.ts +2 -1
- package/dist_ts/smartproxy/classes.smartproxy.js +83 -18
- package/package.json +11 -10
- package/readme.hints.md +64 -1
- package/readme.md +44 -0
- package/readme.plan.md +31 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/networkproxy/classes.np.networkproxy.ts +4 -0
- package/ts/plugins.ts +5 -3
- package/ts/port80handler/classes.port80handler.ts +67 -202
- package/ts/smartproxy/classes.pp.interfaces.ts +10 -0
- package/ts/smartproxy/classes.pp.networkproxybridge.ts +11 -0
- package/ts/smartproxy/classes.smartproxy.ts +79 -22
|
@@ -2,6 +2,21 @@ import * as plugins from '../plugins.js';
|
|
|
2
2
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
+
// ACME HTTP-01 challenge handler storing tokens in memory (diskless)
|
|
6
|
+
class DisklessHttp01Handler {
|
|
7
|
+
private storage: Map<string, string>;
|
|
8
|
+
constructor(storage: Map<string, string>) { this.storage = storage; }
|
|
9
|
+
public getSupportedTypes(): string[] { return ['http-01']; }
|
|
10
|
+
public async prepare(ch: any): Promise<void> {
|
|
11
|
+
this.storage.set(ch.token, ch.keyAuthorization);
|
|
12
|
+
}
|
|
13
|
+
public async verify(ch: any): Promise<void> {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
public async cleanup(ch: any): Promise<void> {
|
|
17
|
+
this.storage.delete(ch.token);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
5
20
|
|
|
6
21
|
/**
|
|
7
22
|
* Custom error classes for better error handling
|
|
@@ -59,8 +74,6 @@ interface IDomainCertificate {
|
|
|
59
74
|
obtainingInProgress: boolean;
|
|
60
75
|
certificate?: string;
|
|
61
76
|
privateKey?: string;
|
|
62
|
-
challengeToken?: string;
|
|
63
|
-
challengeKeyAuthorization?: string;
|
|
64
77
|
expiryDate?: Date;
|
|
65
78
|
lastRenewalAttempt?: Date;
|
|
66
79
|
}
|
|
@@ -128,9 +141,11 @@ export interface ICertificateExpiring {
|
|
|
128
141
|
*/
|
|
129
142
|
export class Port80Handler extends plugins.EventEmitter {
|
|
130
143
|
private domainCertificates: Map<string, IDomainCertificate>;
|
|
144
|
+
// In-memory storage for ACME HTTP-01 challenge tokens
|
|
145
|
+
private acmeHttp01Storage: Map<string, string> = new Map();
|
|
146
|
+
// SmartAcme instance for certificate management
|
|
147
|
+
private smartAcme: plugins.smartacme.SmartAcme | null = null;
|
|
131
148
|
private server: plugins.http.Server | null = null;
|
|
132
|
-
private acmeClient: plugins.acme.Client | null = null;
|
|
133
|
-
private accountKey: string | null = null;
|
|
134
149
|
private renewalTimer: NodeJS.Timeout | null = null;
|
|
135
150
|
private isShuttingDown: boolean = false;
|
|
136
151
|
private options: Required<IPort80HandlerOptions>;
|
|
@@ -175,6 +190,17 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
175
190
|
console.log('Port80Handler is disabled, skipping start');
|
|
176
191
|
return;
|
|
177
192
|
}
|
|
193
|
+
// Initialize SmartAcme for ACME challenge management (diskless HTTP handler)
|
|
194
|
+
if (this.options.enabled) {
|
|
195
|
+
this.smartAcme = new plugins.smartacme.SmartAcme({
|
|
196
|
+
accountEmail: this.options.contactEmail,
|
|
197
|
+
certManager: new plugins.smartacme.MemoryCertManager(),
|
|
198
|
+
environment: this.options.useProduction ? 'production' : 'integration',
|
|
199
|
+
challengeHandlers: [ new DisklessHttp01Handler(this.acmeHttp01Storage) ],
|
|
200
|
+
challengePriority: ['http-01'],
|
|
201
|
+
});
|
|
202
|
+
await this.smartAcme.start();
|
|
203
|
+
}
|
|
178
204
|
|
|
179
205
|
return new Promise((resolve, reject) => {
|
|
180
206
|
try {
|
|
@@ -579,38 +605,6 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
579
605
|
}
|
|
580
606
|
}
|
|
581
607
|
|
|
582
|
-
/**
|
|
583
|
-
* Lazy initialization of the ACME client
|
|
584
|
-
* @returns An ACME client instance
|
|
585
|
-
*/
|
|
586
|
-
private async getAcmeClient(): Promise<plugins.acme.Client> {
|
|
587
|
-
if (this.acmeClient) {
|
|
588
|
-
return this.acmeClient;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
try {
|
|
592
|
-
// Generate a new account key
|
|
593
|
-
this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString();
|
|
594
|
-
|
|
595
|
-
this.acmeClient = new plugins.acme.Client({
|
|
596
|
-
directoryUrl: this.options.useProduction
|
|
597
|
-
? plugins.acme.directory.letsencrypt.production
|
|
598
|
-
: plugins.acme.directory.letsencrypt.staging,
|
|
599
|
-
accountKey: this.accountKey,
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
// Create a new account
|
|
603
|
-
await this.acmeClient.createAccount({
|
|
604
|
-
termsOfServiceAgreed: true,
|
|
605
|
-
contact: [`mailto:${this.options.contactEmail}`],
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
return this.acmeClient;
|
|
609
|
-
} catch (error) {
|
|
610
|
-
const message = error instanceof Error ? error.message : 'Unknown error initializing ACME client';
|
|
611
|
-
throw new Port80HandlerError(`Failed to initialize ACME client: ${message}`);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
608
|
|
|
615
609
|
/**
|
|
616
610
|
* Handles incoming HTTP requests
|
|
@@ -640,19 +634,26 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
640
634
|
const { domainInfo, pattern } = domainMatch;
|
|
641
635
|
const options = domainInfo.options;
|
|
642
636
|
|
|
643
|
-
//
|
|
644
|
-
if (req.url && req.url.startsWith('/.well-known/acme-challenge/') &&
|
|
645
|
-
//
|
|
637
|
+
// Serve or forward ACME HTTP-01 challenge requests
|
|
638
|
+
if (req.url && req.url.startsWith('/.well-known/acme-challenge/') && options.acmeMaintenance) {
|
|
639
|
+
// Forward ACME requests if configured
|
|
646
640
|
if (options.acmeForward) {
|
|
647
641
|
this.forwardRequest(req, res, options.acmeForward, 'ACME challenge');
|
|
648
642
|
return;
|
|
649
643
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
644
|
+
// Serve challenge response from in-memory storage
|
|
645
|
+
const token = req.url.split('/').pop() || '';
|
|
646
|
+
const keyAuth = this.acmeHttp01Storage.get(token);
|
|
647
|
+
if (keyAuth) {
|
|
648
|
+
res.statusCode = 200;
|
|
649
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
650
|
+
res.end(keyAuth);
|
|
651
|
+
console.log(`Served ACME challenge response for ${domain}`);
|
|
652
|
+
} else {
|
|
653
|
+
res.statusCode = 404;
|
|
654
|
+
res.end('Challenge token not found');
|
|
655
655
|
}
|
|
656
|
+
return;
|
|
656
657
|
}
|
|
657
658
|
|
|
658
659
|
// Check if we should forward non-ACME requests
|
|
@@ -762,209 +763,73 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
762
763
|
}
|
|
763
764
|
}
|
|
764
765
|
|
|
765
|
-
/**
|
|
766
|
-
* Serves the ACME HTTP-01 challenge response
|
|
767
|
-
* @param req The HTTP request
|
|
768
|
-
* @param res The HTTP response
|
|
769
|
-
* @param domain The domain for the challenge
|
|
770
|
-
*/
|
|
771
|
-
private handleAcmeChallenge(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, domain: string): void {
|
|
772
|
-
const domainInfo = this.domainCertificates.get(domain);
|
|
773
|
-
if (!domainInfo) {
|
|
774
|
-
res.statusCode = 404;
|
|
775
|
-
res.end('Domain not configured');
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// The token is the last part of the URL
|
|
780
|
-
const urlParts = req.url?.split('/');
|
|
781
|
-
const token = urlParts ? urlParts[urlParts.length - 1] : '';
|
|
782
|
-
|
|
783
|
-
if (domainInfo.challengeToken === token && domainInfo.challengeKeyAuthorization) {
|
|
784
|
-
res.statusCode = 200;
|
|
785
|
-
res.setHeader('Content-Type', 'text/plain');
|
|
786
|
-
res.end(domainInfo.challengeKeyAuthorization);
|
|
787
|
-
console.log(`Served ACME challenge response for ${domain}`);
|
|
788
|
-
} else {
|
|
789
|
-
res.statusCode = 404;
|
|
790
|
-
res.end('Challenge token not found');
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
766
|
|
|
794
767
|
/**
|
|
795
768
|
* Obtains a certificate for a domain using ACME HTTP-01 challenge
|
|
796
769
|
* @param domain The domain to obtain a certificate for
|
|
797
770
|
* @param isRenewal Whether this is a renewal attempt
|
|
798
771
|
*/
|
|
772
|
+
/**
|
|
773
|
+
* Obtains a certificate for a domain using SmartAcme HTTP-01 challenges
|
|
774
|
+
* @param domain The domain to obtain a certificate for
|
|
775
|
+
* @param isRenewal Whether this is a renewal attempt
|
|
776
|
+
*/
|
|
799
777
|
private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise<void> {
|
|
800
|
-
// Don't allow certificate issuance for glob patterns
|
|
801
778
|
if (this.isGlobPattern(domain)) {
|
|
802
779
|
throw new CertificateError('Cannot obtain certificates for glob pattern domains', domain, isRenewal);
|
|
803
780
|
}
|
|
804
|
-
|
|
805
|
-
// Get the domain info
|
|
806
|
-
const domainInfo = this.domainCertificates.get(domain);
|
|
807
|
-
if (!domainInfo) {
|
|
808
|
-
throw new CertificateError('Domain not found', domain, isRenewal);
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Verify that acmeMaintenance is enabled
|
|
781
|
+
const domainInfo = this.domainCertificates.get(domain)!;
|
|
812
782
|
if (!domainInfo.options.acmeMaintenance) {
|
|
813
783
|
console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`);
|
|
814
784
|
return;
|
|
815
785
|
}
|
|
816
|
-
|
|
817
|
-
// Prevent concurrent certificate issuance
|
|
818
786
|
if (domainInfo.obtainingInProgress) {
|
|
819
787
|
console.log(`Certificate issuance already in progress for ${domain}`);
|
|
820
788
|
return;
|
|
821
789
|
}
|
|
822
|
-
|
|
790
|
+
if (!this.smartAcme) {
|
|
791
|
+
throw new Port80HandlerError('SmartAcme is not initialized');
|
|
792
|
+
}
|
|
823
793
|
domainInfo.obtainingInProgress = true;
|
|
824
794
|
domainInfo.lastRenewalAttempt = new Date();
|
|
825
|
-
|
|
826
795
|
try {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
// Get the authorizations for the order
|
|
835
|
-
const authorizations = await client.getAuthorizations(order);
|
|
836
|
-
|
|
837
|
-
// Process each authorization
|
|
838
|
-
await this.processAuthorizations(client, domain, authorizations);
|
|
839
|
-
|
|
840
|
-
// Generate a CSR and private key
|
|
841
|
-
const [csrBuffer, privateKeyBuffer] = await plugins.acme.forge.createCsr({
|
|
842
|
-
commonName: domain,
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
const csr = csrBuffer.toString();
|
|
846
|
-
const privateKey = privateKeyBuffer.toString();
|
|
847
|
-
|
|
848
|
-
// Finalize the order with our CSR
|
|
849
|
-
await client.finalizeOrder(order, csr);
|
|
850
|
-
|
|
851
|
-
// Get the certificate with the full chain
|
|
852
|
-
const certificate = await client.getCertificate(order);
|
|
853
|
-
|
|
854
|
-
// Store the certificate and key
|
|
796
|
+
// Request certificate via SmartAcme
|
|
797
|
+
const certObj = await this.smartAcme.getCertificateForDomain(domain);
|
|
798
|
+
const certificate = certObj.publicKey;
|
|
799
|
+
const privateKey = certObj.privateKey;
|
|
800
|
+
const expiryDate = new Date(certObj.validUntil);
|
|
855
801
|
domainInfo.certificate = certificate;
|
|
856
802
|
domainInfo.privateKey = privateKey;
|
|
857
803
|
domainInfo.certObtained = true;
|
|
858
|
-
|
|
859
|
-
// Clear challenge data
|
|
860
|
-
delete domainInfo.challengeToken;
|
|
861
|
-
delete domainInfo.challengeKeyAuthorization;
|
|
862
|
-
|
|
863
|
-
// Extract expiry date from certificate
|
|
864
|
-
domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain);
|
|
804
|
+
domainInfo.expiryDate = expiryDate;
|
|
865
805
|
|
|
866
806
|
console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
|
|
867
|
-
|
|
868
|
-
// Save the certificate to the store if enabled
|
|
869
807
|
if (this.options.certificateStore) {
|
|
870
808
|
this.saveCertificateToStore(domain, certificate, privateKey);
|
|
871
809
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
const eventType = isRenewal
|
|
875
|
-
? Port80HandlerEvents.CERTIFICATE_RENEWED
|
|
810
|
+
const eventType = isRenewal
|
|
811
|
+
? Port80HandlerEvents.CERTIFICATE_RENEWED
|
|
876
812
|
: Port80HandlerEvents.CERTIFICATE_ISSUED;
|
|
877
|
-
|
|
878
813
|
this.emitCertificateEvent(eventType, {
|
|
879
814
|
domain,
|
|
880
815
|
certificate,
|
|
881
816
|
privateKey,
|
|
882
|
-
expiryDate:
|
|
817
|
+
expiryDate: expiryDate || this.getDefaultExpiryDate()
|
|
883
818
|
});
|
|
884
|
-
|
|
885
819
|
} catch (error: any) {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
error.message.includes('rateLimited') ||
|
|
889
|
-
error.message.includes('too many certificates') ||
|
|
890
|
-
error.message.includes('rate limit')
|
|
891
|
-
)) {
|
|
892
|
-
console.error(`Rate limit reached for ${domain}. Waiting before retry.`);
|
|
893
|
-
} else {
|
|
894
|
-
console.error(`Error during certificate issuance for ${domain}:`, error);
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// Emit failure event
|
|
820
|
+
const errorMsg = error?.message || 'Unknown error';
|
|
821
|
+
console.error(`Error during certificate issuance for ${domain}:`, error);
|
|
898
822
|
this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, {
|
|
899
823
|
domain,
|
|
900
|
-
error:
|
|
824
|
+
error: errorMsg,
|
|
901
825
|
isRenewal
|
|
902
826
|
} as ICertificateFailure);
|
|
903
|
-
|
|
904
|
-
throw new CertificateError(
|
|
905
|
-
error.message || 'Certificate issuance failed',
|
|
906
|
-
domain,
|
|
907
|
-
isRenewal
|
|
908
|
-
);
|
|
827
|
+
throw new CertificateError(errorMsg, domain, isRenewal);
|
|
909
828
|
} finally {
|
|
910
|
-
// Reset flag whether successful or not
|
|
911
829
|
domainInfo.obtainingInProgress = false;
|
|
912
830
|
}
|
|
913
831
|
}
|
|
914
832
|
|
|
915
|
-
/**
|
|
916
|
-
* Process ACME authorizations by verifying and completing challenges
|
|
917
|
-
* @param client ACME client
|
|
918
|
-
* @param domain Domain name
|
|
919
|
-
* @param authorizations Authorizations to process
|
|
920
|
-
*/
|
|
921
|
-
private async processAuthorizations(
|
|
922
|
-
client: plugins.acme.Client,
|
|
923
|
-
domain: string,
|
|
924
|
-
authorizations: plugins.acme.Authorization[]
|
|
925
|
-
): Promise<void> {
|
|
926
|
-
const domainInfo = this.domainCertificates.get(domain);
|
|
927
|
-
if (!domainInfo) {
|
|
928
|
-
throw new CertificateError('Domain not found during authorization', domain);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
for (const authz of authorizations) {
|
|
932
|
-
const challenge = authz.challenges.find(ch => ch.type === 'http-01');
|
|
933
|
-
if (!challenge) {
|
|
934
|
-
throw new CertificateError('HTTP-01 challenge not found', domain);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
// Get the key authorization for the challenge
|
|
938
|
-
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
|
939
|
-
|
|
940
|
-
// Store the challenge data
|
|
941
|
-
domainInfo.challengeToken = challenge.token;
|
|
942
|
-
domainInfo.challengeKeyAuthorization = keyAuthorization;
|
|
943
|
-
|
|
944
|
-
// ACME client type definition workaround - use compatible approach
|
|
945
|
-
// First check if challenge verification is needed
|
|
946
|
-
const authzUrl = authz.url;
|
|
947
|
-
|
|
948
|
-
try {
|
|
949
|
-
// Check if authzUrl exists and perform verification
|
|
950
|
-
if (authzUrl) {
|
|
951
|
-
await client.verifyChallenge(authz, challenge);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// Complete the challenge
|
|
955
|
-
await client.completeChallenge(challenge);
|
|
956
|
-
|
|
957
|
-
// Wait for validation
|
|
958
|
-
await client.waitForValidStatus(challenge);
|
|
959
|
-
console.log(`HTTP-01 challenge completed for ${domain}`);
|
|
960
|
-
} catch (error) {
|
|
961
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown challenge error';
|
|
962
|
-
console.error(`Challenge error for ${domain}:`, error);
|
|
963
|
-
throw new CertificateError(`Challenge verification failed: ${errorMessage}`, domain);
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
|
|
968
833
|
/**
|
|
969
834
|
* Starts the certificate renewal timer
|
|
970
835
|
*/
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Provision object for static or HTTP-01 certificate
|
|
5
|
+
*/
|
|
6
|
+
export type ISmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01';
|
|
7
|
+
|
|
3
8
|
/** Domain configuration with per-domain allowed port ranges */
|
|
4
9
|
export interface IDomainConfig {
|
|
5
10
|
domains: string[]; // Glob patterns for domain(s)
|
|
@@ -115,6 +120,11 @@ export interface IPortProxySettings {
|
|
|
115
120
|
certificateStore?: string;
|
|
116
121
|
skipConfiguredCerts?: boolean;
|
|
117
122
|
};
|
|
123
|
+
/**
|
|
124
|
+
* Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges,
|
|
125
|
+
* or a static certificate object for immediate provisioning.
|
|
126
|
+
*/
|
|
127
|
+
certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
/**
|
|
@@ -95,6 +95,17 @@ export class NetworkProxyBridge {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Apply an external (static) certificate into NetworkProxy
|
|
100
|
+
*/
|
|
101
|
+
public applyExternalCertificate(data: ICertificateData): void {
|
|
102
|
+
if (!this.networkProxy) {
|
|
103
|
+
console.log(`NetworkProxy not initialized: cannot apply external certificate for ${data.domain}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.handleCertificateEvent(data);
|
|
107
|
+
}
|
|
108
|
+
|
|
98
109
|
/**
|
|
99
110
|
* Get the NetworkProxy instance
|
|
100
111
|
*/
|
|
@@ -8,14 +8,14 @@ import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
|
|
|
8
8
|
import { TimeoutManager } from './classes.pp.timeoutmanager.js';
|
|
9
9
|
import { PortRangeManager } from './classes.pp.portrangemanager.js';
|
|
10
10
|
import { ConnectionHandler } from './classes.pp.connectionhandler.js';
|
|
11
|
-
import { Port80Handler, Port80HandlerEvents } from '../port80handler/classes.port80handler.js';
|
|
11
|
+
import { Port80Handler, Port80HandlerEvents, type ICertificateData } from '../port80handler/classes.port80handler.js';
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* SmartProxy - Main class that coordinates all components
|
|
17
17
|
*/
|
|
18
|
-
export class SmartProxy {
|
|
18
|
+
export class SmartProxy extends plugins.EventEmitter {
|
|
19
19
|
private netServers: plugins.net.Server[] = [];
|
|
20
20
|
private connectionLogger: NodeJS.Timeout | null = null;
|
|
21
21
|
private isShuttingDown: boolean = false;
|
|
@@ -34,6 +34,7 @@ export class SmartProxy {
|
|
|
34
34
|
private port80Handler: Port80Handler | null = null;
|
|
35
35
|
|
|
36
36
|
constructor(settingsArg: IPortProxySettings) {
|
|
37
|
+
super();
|
|
37
38
|
// Set reasonable defaults for all settings
|
|
38
39
|
this.settings = {
|
|
39
40
|
...settingsArg,
|
|
@@ -180,29 +181,67 @@ export class SmartProxy {
|
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
183
|
-
//
|
|
184
|
+
// Provision certificates per domain via certProvider or HTTP-01
|
|
184
185
|
for (const domainConfig of this.settings.domainConfigs) {
|
|
185
186
|
for (const domain of domainConfig.domains) {
|
|
186
|
-
// Skip
|
|
187
|
+
// Skip wildcard domains
|
|
187
188
|
if (domain.includes('*')) continue;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
189
|
+
// Determine provisioning method
|
|
190
|
+
let provision = 'http01' as string | plugins.tsclass.network.ICert;
|
|
191
|
+
if (this.settings.certProvider) {
|
|
192
|
+
try {
|
|
193
|
+
provision = await this.settings.certProvider(domain);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.log(`certProvider error for ${domain}: ${err}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (provision === 'http01') {
|
|
199
|
+
this.port80Handler.addDomain({
|
|
200
|
+
domainName: domain,
|
|
201
|
+
sslRedirect: true,
|
|
202
|
+
acmeMaintenance: true
|
|
203
|
+
});
|
|
204
|
+
console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
|
|
205
|
+
} else {
|
|
206
|
+
// Static certificate provided
|
|
207
|
+
const certObj = provision as plugins.tsclass.network.ICert;
|
|
208
|
+
const certData: ICertificateData = {
|
|
209
|
+
domain: certObj.domainName,
|
|
210
|
+
certificate: certObj.publicKey,
|
|
211
|
+
privateKey: certObj.privateKey,
|
|
212
|
+
expiryDate: new Date(certObj.validUntil)
|
|
213
|
+
};
|
|
214
|
+
this.networkProxyBridge.applyExternalCertificate(certData);
|
|
215
|
+
console.log(`Applied static certificate for ${domain} from certProvider`);
|
|
216
|
+
}
|
|
196
217
|
}
|
|
197
218
|
}
|
|
198
219
|
|
|
199
220
|
// Set up event listeners
|
|
200
221
|
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, (certData) => {
|
|
201
222
|
console.log(`Certificate issued for ${certData.domain}, valid until ${certData.expiryDate.toISOString()}`);
|
|
223
|
+
// Re-emit on SmartProxy
|
|
224
|
+
this.emit('certificate', {
|
|
225
|
+
domain: certData.domain,
|
|
226
|
+
publicKey: certData.certificate,
|
|
227
|
+
privateKey: certData.privateKey,
|
|
228
|
+
expiryDate: certData.expiryDate,
|
|
229
|
+
source: 'http01',
|
|
230
|
+
isRenewal: false
|
|
231
|
+
});
|
|
202
232
|
});
|
|
203
233
|
|
|
204
234
|
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, (certData) => {
|
|
205
235
|
console.log(`Certificate renewed for ${certData.domain}, valid until ${certData.expiryDate.toISOString()}`);
|
|
236
|
+
// Re-emit on SmartProxy
|
|
237
|
+
this.emit('certificate', {
|
|
238
|
+
domain: certData.domain,
|
|
239
|
+
publicKey: certData.certificate,
|
|
240
|
+
privateKey: certData.privateKey,
|
|
241
|
+
expiryDate: certData.expiryDate,
|
|
242
|
+
source: 'http01',
|
|
243
|
+
isRenewal: true
|
|
244
|
+
});
|
|
206
245
|
});
|
|
207
246
|
|
|
208
247
|
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, (failureData) => {
|
|
@@ -429,22 +468,40 @@ export class SmartProxy {
|
|
|
429
468
|
await this.networkProxyBridge.syncDomainConfigsToNetworkProxy();
|
|
430
469
|
}
|
|
431
470
|
|
|
432
|
-
// If Port80Handler is running,
|
|
471
|
+
// If Port80Handler is running, provision certificates per new domain
|
|
433
472
|
if (this.port80Handler && this.settings.port80HandlerConfig?.enabled) {
|
|
434
473
|
for (const domainConfig of newDomainConfigs) {
|
|
435
474
|
for (const domain of domainConfig.domains) {
|
|
436
|
-
// Skip wildcards
|
|
437
475
|
if (domain.includes('*')) continue;
|
|
438
|
-
|
|
439
|
-
this.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
476
|
+
let provision = 'http01' as string | plugins.tsclass.network.ICert;
|
|
477
|
+
if (this.settings.certProvider) {
|
|
478
|
+
try {
|
|
479
|
+
provision = await this.settings.certProvider(domain);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
console.log(`certProvider error for ${domain}: ${err}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (provision === 'http01') {
|
|
485
|
+
this.port80Handler.addDomain({
|
|
486
|
+
domainName: domain,
|
|
487
|
+
sslRedirect: true,
|
|
488
|
+
acmeMaintenance: true
|
|
489
|
+
});
|
|
490
|
+
console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
|
|
491
|
+
} else {
|
|
492
|
+
const certObj = provision as plugins.tsclass.network.ICert;
|
|
493
|
+
const certData: ICertificateData = {
|
|
494
|
+
domain: certObj.domainName,
|
|
495
|
+
certificate: certObj.publicKey,
|
|
496
|
+
privateKey: certObj.privateKey,
|
|
497
|
+
expiryDate: new Date(certObj.validUntil)
|
|
498
|
+
};
|
|
499
|
+
this.networkProxyBridge.applyExternalCertificate(certData);
|
|
500
|
+
console.log(`Applied static certificate for ${domain} from certProvider`);
|
|
501
|
+
}
|
|
444
502
|
}
|
|
445
503
|
}
|
|
446
|
-
|
|
447
|
-
console.log('Registered non-wildcard domains with Port80Handler');
|
|
504
|
+
console.log('Provisioned certificates for new domains');
|
|
448
505
|
}
|
|
449
506
|
}
|
|
450
507
|
|