@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.
@@ -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
- // If the request is for an ACME HTTP-01 challenge, handle it
644
- if (req.url && req.url.startsWith('/.well-known/acme-challenge/') && (options.acmeMaintenance || options.acmeForward)) {
645
- // Check if we should forward ACME requests
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
- // Only handle ACME challenges for non-glob patterns
652
- if (!this.isGlobPattern(pattern)) {
653
- this.handleAcmeChallenge(req, res, domain);
654
- return;
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
- const client = await this.getAcmeClient();
828
-
829
- // Create a new order for the domain
830
- const order = await client.createOrder({
831
- identifiers: [{ type: 'dns', value: domain }],
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
- // Emit the appropriate event
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: domainInfo.expiryDate || this.getDefaultExpiryDate()
817
+ expiryDate: expiryDate || this.getDefaultExpiryDate()
883
818
  });
884
-
885
819
  } catch (error: any) {
886
- // Check for rate limit errors
887
- if (error.message && (
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: error.message || 'Unknown 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
- // Register all non-wildcard domains from domain configs
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 wildcards
187
+ // Skip wildcard domains
187
188
  if (domain.includes('*')) continue;
188
-
189
- this.port80Handler.addDomain({
190
- domainName: domain,
191
- sslRedirect: true,
192
- acmeMaintenance: true
193
- });
194
-
195
- console.log(`Registered domain ${domain} with Port80Handler`);
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, register non-wildcard domains
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.port80Handler.addDomain({
440
- domainName: domain,
441
- sslRedirect: true,
442
- acmeMaintenance: true
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