@push.rocks/smartproxy 3.10.4 → 3.11.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.
@@ -0,0 +1,214 @@
1
+ import * as http from 'http';
2
+ import * as acme from 'acme-client';
3
+
4
+ interface IDomainCertificate {
5
+ certObtained: boolean;
6
+ obtainingInProgress: boolean;
7
+ certificate?: string;
8
+ privateKey?: string;
9
+ challengeToken?: string;
10
+ challengeKeyAuthorization?: string;
11
+ }
12
+
13
+ export class Port80Handler {
14
+ private domainCertificates: Map<string, IDomainCertificate>;
15
+ private server: http.Server;
16
+ private acmeClient: acme.Client | null = null;
17
+ private accountKey: string | null = null;
18
+
19
+ constructor() {
20
+ this.domainCertificates = new Map<string, IDomainCertificate>();
21
+
22
+ // Create and start an HTTP server on port 80.
23
+ this.server = http.createServer((req, res) => this.handleRequest(req, res));
24
+ this.server.listen(80, () => {
25
+ console.log('Port80Handler is listening on port 80');
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Adds a domain to be managed.
31
+ * @param domain The domain to add.
32
+ */
33
+ public addDomain(domain: string): void {
34
+ if (!this.domainCertificates.has(domain)) {
35
+ this.domainCertificates.set(domain, { certObtained: false, obtainingInProgress: false });
36
+ console.log(`Domain added: ${domain}`);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Removes a domain from management.
42
+ * @param domain The domain to remove.
43
+ */
44
+ public removeDomain(domain: string): void {
45
+ if (this.domainCertificates.delete(domain)) {
46
+ console.log(`Domain removed: ${domain}`);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Lazy initialization of the ACME client.
52
+ * Uses Let’s Encrypt’s production directory (for testing you might switch to staging).
53
+ */
54
+ private async getAcmeClient(): Promise<acme.Client> {
55
+ if (this.acmeClient) {
56
+ return this.acmeClient;
57
+ }
58
+ // Generate a new account key and convert Buffer to string.
59
+ this.accountKey = (await acme.forge.createPrivateKey()).toString();
60
+ this.acmeClient = new acme.Client({
61
+ directoryUrl: acme.directory.letsencrypt.production, // Use production for a real certificate
62
+ // For testing, you could use:
63
+ // directoryUrl: acme.directory.letsencrypt.staging,
64
+ accountKey: this.accountKey,
65
+ });
66
+ // Create a new account. Make sure to update the contact email.
67
+ await this.acmeClient.createAccount({
68
+ termsOfServiceAgreed: true,
69
+ contact: ['mailto:admin@example.com'],
70
+ });
71
+ return this.acmeClient;
72
+ }
73
+
74
+ /**
75
+ * Handles incoming HTTP requests on port 80.
76
+ * If the request is for an ACME challenge, it responds with the key authorization.
77
+ * If the domain has a certificate, it redirects to HTTPS; otherwise, it initiates certificate issuance.
78
+ */
79
+ private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
80
+ const hostHeader = req.headers.host;
81
+ if (!hostHeader) {
82
+ res.statusCode = 400;
83
+ res.end('Bad Request: Host header is missing');
84
+ return;
85
+ }
86
+ // Extract domain (ignoring any port in the Host header)
87
+ const domain = hostHeader.split(':')[0];
88
+
89
+ // If the request is for an ACME HTTP-01 challenge, handle it.
90
+ if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) {
91
+ this.handleAcmeChallenge(req, res, domain);
92
+ return;
93
+ }
94
+
95
+ if (!this.domainCertificates.has(domain)) {
96
+ res.statusCode = 404;
97
+ res.end('Domain not configured');
98
+ return;
99
+ }
100
+
101
+ const domainInfo = this.domainCertificates.get(domain)!;
102
+
103
+ // If certificate exists, redirect to HTTPS on port 443.
104
+ if (domainInfo.certObtained) {
105
+ const redirectUrl = `https://${domain}:443${req.url}`;
106
+ res.statusCode = 301;
107
+ res.setHeader('Location', redirectUrl);
108
+ res.end(`Redirecting to ${redirectUrl}`);
109
+ } else {
110
+ // Trigger certificate issuance if not already running.
111
+ if (!domainInfo.obtainingInProgress) {
112
+ domainInfo.obtainingInProgress = true;
113
+ this.obtainCertificate(domain).catch(err => {
114
+ console.error(`Error obtaining certificate for ${domain}:`, err);
115
+ });
116
+ }
117
+ res.statusCode = 503;
118
+ res.end('Certificate issuance in progress, please try again later.');
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Serves the ACME HTTP-01 challenge response.
124
+ */
125
+ private handleAcmeChallenge(req: http.IncomingMessage, res: http.ServerResponse, domain: string): void {
126
+ const domainInfo = this.domainCertificates.get(domain);
127
+ if (!domainInfo) {
128
+ res.statusCode = 404;
129
+ res.end('Domain not configured');
130
+ return;
131
+ }
132
+ // The token is the last part of the URL.
133
+ const urlParts = req.url?.split('/');
134
+ const token = urlParts ? urlParts[urlParts.length - 1] : '';
135
+ if (domainInfo.challengeToken === token && domainInfo.challengeKeyAuthorization) {
136
+ res.statusCode = 200;
137
+ res.setHeader('Content-Type', 'text/plain');
138
+ res.end(domainInfo.challengeKeyAuthorization);
139
+ console.log(`Served ACME challenge response for ${domain}`);
140
+ } else {
141
+ res.statusCode = 404;
142
+ res.end('Challenge token not found');
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Uses acme-client to perform a full ACME HTTP-01 challenge to obtain a certificate.
148
+ * On success, it stores the certificate and key in memory and clears challenge data.
149
+ */
150
+ private async obtainCertificate(domain: string): Promise<void> {
151
+ try {
152
+ const client = await this.getAcmeClient();
153
+
154
+ // Create a new order for the domain.
155
+ const order = await client.createOrder({
156
+ identifiers: [{ type: 'dns', value: domain }],
157
+ });
158
+
159
+ // Get the authorizations for the order.
160
+ const authorizations = await client.getAuthorizations(order);
161
+ for (const authz of authorizations) {
162
+ const challenge = authz.challenges.find(ch => ch.type === 'http-01');
163
+ if (!challenge) {
164
+ throw new Error('HTTP-01 challenge not found');
165
+ }
166
+ // Get the key authorization for the challenge.
167
+ const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
168
+ const domainInfo = this.domainCertificates.get(domain)!;
169
+ domainInfo.challengeToken = challenge.token;
170
+ domainInfo.challengeKeyAuthorization = keyAuthorization;
171
+
172
+ // Notify the ACME server that the challenge is ready.
173
+ // The acme-client examples show that verifyChallenge takes three arguments:
174
+ // (authorization, challenge, keyAuthorization). However, the official TypeScript
175
+ // types appear to be out-of-sync. As a workaround, we cast client to 'any'.
176
+ await (client as any).verifyChallenge(authz, challenge, keyAuthorization);
177
+
178
+ await client.completeChallenge(challenge);
179
+ // Wait until the challenge is validated.
180
+ await client.waitForValidStatus(challenge);
181
+ console.log(`HTTP-01 challenge completed for ${domain}`);
182
+ }
183
+
184
+ // Generate a CSR and a new private key for the domain.
185
+ // Convert the resulting Buffers to strings.
186
+ const [csrBuffer, privateKeyBuffer] = await acme.forge.createCsr({
187
+ commonName: domain,
188
+ });
189
+ const csr = csrBuffer.toString();
190
+ const privateKey = privateKeyBuffer.toString();
191
+
192
+ // Finalize the order and obtain the certificate.
193
+ await client.finalizeOrder(order, csr);
194
+ const certificate = await client.getCertificate(order);
195
+
196
+ const domainInfo = this.domainCertificates.get(domain)!;
197
+ domainInfo.certificate = certificate;
198
+ domainInfo.privateKey = privateKey;
199
+ domainInfo.certObtained = true;
200
+ domainInfo.obtainingInProgress = false;
201
+ delete domainInfo.challengeToken;
202
+ delete domainInfo.challengeKeyAuthorization;
203
+
204
+ console.log(`Certificate obtained for ${domain}`);
205
+ // In a production system, persist the certificate and key and reload your TLS server.
206
+ } catch (error) {
207
+ console.error(`Error during certificate issuance for ${domain}:`, error);
208
+ const domainInfo = this.domainCertificates.get(domain);
209
+ if (domainInfo) {
210
+ domainInfo.obtainingInProgress = false;
211
+ }
212
+ }
213
+ }
214
+ }
package/ts/index.ts CHANGED
@@ -1,3 +1,4 @@
1
- export * from './smartproxy.classes.networkproxy.js';
2
- export * from './smartproxy.portproxy.js';
3
- export * from './smartproxy.classes.sslredirect.js';
1
+ export * from './classes.networkproxy.js';
2
+ export * from './classes.portproxy.js';
3
+ export * from './classes.port80handler.js';
4
+ export * from './classes.sslredirect.js';