advanced-tls-client 1.0.2 → 3.0.2
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/index.d.ts +60 -24
- package/dist/index.js +615 -263
- package/package.json +1 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/readme.md +0 -375
package/dist/index.js
CHANGED
|
@@ -42,9 +42,12 @@ const events_1 = require("events");
|
|
|
42
42
|
const zlib = __importStar(require("zlib"));
|
|
43
43
|
const util_1 = require("util");
|
|
44
44
|
const stream_1 = require("stream");
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const url = __importStar(require("url"));
|
|
48
|
+
const brotliDecompressAsync = (0, util_1.promisify)(zlib.brotliDecompress);
|
|
49
|
+
const gunzipAsync = (0, util_1.promisify)(zlib.gunzip);
|
|
50
|
+
const inflateAsync = (0, util_1.promisify)(zlib.inflate);
|
|
48
51
|
const GREASE_VALUES = [0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a, 0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa];
|
|
49
52
|
class ProfileRegistry {
|
|
50
53
|
static getGrease() {
|
|
@@ -134,8 +137,8 @@ class ProfileRegistry {
|
|
|
134
137
|
headerTableSize: 65536,
|
|
135
138
|
enablePush: 0,
|
|
136
139
|
initialWindowSize: 6291456,
|
|
137
|
-
maxHeaderListSize: 262144,
|
|
138
140
|
maxFrameSize: 16384,
|
|
141
|
+
maxHeaderListSize: 262144,
|
|
139
142
|
settingsOrder: [1, 2, 4, 6],
|
|
140
143
|
connectionPreface: Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'),
|
|
141
144
|
priorityFrames: [
|
|
@@ -640,147 +643,65 @@ class CookieJar {
|
|
|
640
643
|
return validCookies.join('; ');
|
|
641
644
|
}
|
|
642
645
|
parseSetCookie(domain, setCookieHeaders) {
|
|
643
|
-
|
|
646
|
+
if (!setCookieHeaders)
|
|
647
|
+
return;
|
|
648
|
+
const headers = Array.isArray(setCookieHeaders) ? setCookieHeaders : typeof setCookieHeaders === 'string' ? [setCookieHeaders] : [];
|
|
649
|
+
for (const header of headers) {
|
|
644
650
|
const parts = header.split(';').map(p => p.trim());
|
|
645
651
|
const [nameValue] = parts;
|
|
646
|
-
|
|
652
|
+
if (!nameValue)
|
|
653
|
+
continue;
|
|
654
|
+
const [name, value = ''] = nameValue.split('=');
|
|
647
655
|
const options = { path: '/' };
|
|
648
656
|
for (let i = 1; i < parts.length; i++) {
|
|
649
|
-
const
|
|
657
|
+
const part = parts[i];
|
|
658
|
+
if (!part)
|
|
659
|
+
continue;
|
|
660
|
+
const [key, val] = part.split('=');
|
|
650
661
|
const lowerKey = key.toLowerCase();
|
|
651
|
-
if (lowerKey === 'expires')
|
|
662
|
+
if (lowerKey === 'expires' && val)
|
|
652
663
|
options.expires = new Date(val);
|
|
653
|
-
else if (lowerKey === 'path')
|
|
664
|
+
else if (lowerKey === 'path' && val)
|
|
654
665
|
options.path = val;
|
|
655
666
|
else if (lowerKey === 'secure')
|
|
656
667
|
options.secure = true;
|
|
657
668
|
else if (lowerKey === 'httponly')
|
|
658
669
|
options.httpOnly = true;
|
|
659
|
-
else if (lowerKey === 'samesite')
|
|
670
|
+
else if (lowerKey === 'samesite' && val)
|
|
660
671
|
options.sameSite = val;
|
|
661
672
|
}
|
|
662
673
|
this.setCookie(domain, name, value, options);
|
|
663
674
|
}
|
|
664
675
|
}
|
|
665
676
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
this.startTime = 0;
|
|
672
|
-
this.hostname = '';
|
|
673
|
-
this.port = 443;
|
|
674
|
-
this.profile = profile;
|
|
675
|
-
this.timing = { socket: 0, lookup: 0, connect: 0, secureConnect: 0, response: 0, end: 0, total: 0 };
|
|
676
|
-
}
|
|
677
|
-
async connect(hostname, port = 443) {
|
|
678
|
-
this.hostname = hostname;
|
|
679
|
-
this.port = port;
|
|
680
|
-
this.startTime = Date.now();
|
|
681
|
-
return new Promise((resolve, reject) => {
|
|
682
|
-
const timeout = setTimeout(() => {
|
|
683
|
-
this.destroy();
|
|
684
|
-
reject(new Error('Connection timeout'));
|
|
685
|
-
}, 30000);
|
|
686
|
-
this.socket = new net.Socket();
|
|
687
|
-
this.socket.setNoDelay(true);
|
|
688
|
-
this.socket.setKeepAlive(true, 60000);
|
|
689
|
-
this.socket.on('lookup', () => {
|
|
690
|
-
this.timing.lookup = Date.now() - this.startTime;
|
|
691
|
-
});
|
|
692
|
-
this.socket.connect(port, hostname, () => {
|
|
693
|
-
this.timing.connect = Date.now() - this.startTime;
|
|
694
|
-
const validCiphers = this.profile.tls.cipherSuites
|
|
695
|
-
.filter(c => c < 0xff00)
|
|
696
|
-
.map(c => {
|
|
697
|
-
const hex = c.toString(16).toUpperCase();
|
|
698
|
-
return hex.length === 3 ? `0${hex}` : hex;
|
|
699
|
-
});
|
|
700
|
-
const cipherList = [
|
|
701
|
-
'TLS_AES_128_GCM_SHA256',
|
|
702
|
-
'TLS_AES_256_GCM_SHA384',
|
|
703
|
-
'TLS_CHACHA20_POLY1305_SHA256',
|
|
704
|
-
'ECDHE-ECDSA-AES128-GCM-SHA256',
|
|
705
|
-
'ECDHE-RSA-AES128-GCM-SHA256',
|
|
706
|
-
'ECDHE-ECDSA-AES256-GCM-SHA384',
|
|
707
|
-
'ECDHE-RSA-AES256-GCM-SHA384',
|
|
708
|
-
'ECDHE-ECDSA-CHACHA20-POLY1305',
|
|
709
|
-
'ECDHE-RSA-CHACHA20-POLY1305',
|
|
710
|
-
'ECDHE-RSA-AES128-SHA',
|
|
711
|
-
'ECDHE-RSA-AES256-SHA',
|
|
712
|
-
'AES128-GCM-SHA256',
|
|
713
|
-
'AES256-GCM-SHA384',
|
|
714
|
-
'AES128-SHA',
|
|
715
|
-
'AES256-SHA'
|
|
716
|
-
].join(':');
|
|
717
|
-
const tlsOptions = {
|
|
718
|
-
socket: this.socket,
|
|
719
|
-
servername: hostname,
|
|
720
|
-
ALPNProtocols: this.profile.tls.alpnProtocols,
|
|
721
|
-
ciphers: cipherList,
|
|
722
|
-
minVersion: 'TLSv1.2',
|
|
723
|
-
maxVersion: 'TLSv1.3',
|
|
724
|
-
rejectUnauthorized: false,
|
|
725
|
-
requestCert: false,
|
|
726
|
-
honorCipherOrder: true,
|
|
727
|
-
sessionTimeout: 300
|
|
728
|
-
};
|
|
729
|
-
this.tlsSocket = tls.connect(tlsOptions);
|
|
730
|
-
this.tlsSocket.once('secureConnect', () => {
|
|
731
|
-
clearTimeout(timeout);
|
|
732
|
-
this.timing.secureConnect = Date.now() - this.startTime;
|
|
733
|
-
this.timing.socket = this.startTime;
|
|
734
|
-
resolve(this.tlsSocket);
|
|
735
|
-
});
|
|
736
|
-
this.tlsSocket.on('error', (err) => {
|
|
737
|
-
clearTimeout(timeout);
|
|
738
|
-
this.destroy();
|
|
739
|
-
reject(err);
|
|
740
|
-
});
|
|
741
|
-
});
|
|
742
|
-
this.socket.on('error', (err) => {
|
|
743
|
-
clearTimeout(timeout);
|
|
744
|
-
this.destroy();
|
|
745
|
-
reject(err);
|
|
746
|
-
});
|
|
747
|
-
this.socket.on('timeout', () => {
|
|
748
|
-
clearTimeout(timeout);
|
|
749
|
-
this.destroy();
|
|
750
|
-
reject(new Error('Socket timeout'));
|
|
751
|
-
});
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
getTiming() {
|
|
755
|
-
return { ...this.timing, total: Date.now() - this.startTime };
|
|
756
|
-
}
|
|
757
|
-
destroy() {
|
|
758
|
-
if (this.tlsSocket && !this.tlsSocket.destroyed) {
|
|
759
|
-
this.tlsSocket.destroy();
|
|
760
|
-
}
|
|
761
|
-
if (this.socket && !this.socket.destroyed) {
|
|
762
|
-
this.socket.destroy();
|
|
763
|
-
}
|
|
764
|
-
this.tlsSocket = null;
|
|
765
|
-
this.socket = null;
|
|
766
|
-
}
|
|
767
|
-
getSocket() {
|
|
768
|
-
return this.tlsSocket;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
class HTTP2ClientManager extends events_1.EventEmitter {
|
|
677
|
+
const Protocol = {
|
|
678
|
+
HTTP1: 'http1',
|
|
679
|
+
HTTP2: 'http2'
|
|
680
|
+
};
|
|
681
|
+
class UnifiedClientManager extends events_1.EventEmitter {
|
|
772
682
|
constructor(tlsSocket, profile, hostname, cookieJar) {
|
|
773
683
|
super();
|
|
774
|
-
this.
|
|
684
|
+
this.http2Client = null;
|
|
685
|
+
this.negotiatedProtocol = 'http1';
|
|
775
686
|
this.tlsSocket = tlsSocket;
|
|
776
687
|
this.profile = profile;
|
|
777
688
|
this.hostname = hostname;
|
|
778
689
|
this.cookieJar = cookieJar;
|
|
779
690
|
}
|
|
780
|
-
async
|
|
691
|
+
async initialize() {
|
|
692
|
+
const alpn = this.tlsSocket.alpnProtocol;
|
|
693
|
+
if (alpn === 'h2') {
|
|
694
|
+
this.negotiatedProtocol = 'http2';
|
|
695
|
+
await this.createHttp2Session();
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
this.negotiatedProtocol = 'http1';
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async createHttp2Session() {
|
|
781
702
|
return new Promise((resolve, reject) => {
|
|
782
703
|
const settings = {};
|
|
783
|
-
this.profile.http2.settingsOrder.forEach(id => {
|
|
704
|
+
this.profile.http2.settingsOrder.forEach((id) => {
|
|
784
705
|
switch (id) {
|
|
785
706
|
case 1:
|
|
786
707
|
settings.headerTableSize = this.profile.http2.headerTableSize;
|
|
@@ -788,171 +709,396 @@ class HTTP2ClientManager extends events_1.EventEmitter {
|
|
|
788
709
|
case 2:
|
|
789
710
|
settings.enablePush = this.profile.http2.enablePush === 1;
|
|
790
711
|
break;
|
|
791
|
-
case 3:
|
|
792
|
-
if (this.profile.http2.maxConcurrentStreams)
|
|
793
|
-
settings.maxConcurrentStreams = this.profile.http2.maxConcurrentStreams;
|
|
794
|
-
break;
|
|
795
712
|
case 4:
|
|
796
713
|
settings.initialWindowSize = this.profile.http2.initialWindowSize;
|
|
797
714
|
break;
|
|
798
|
-
case 5:
|
|
799
|
-
if (this.profile.http2.maxFrameSize)
|
|
800
|
-
settings.maxFrameSize = this.profile.http2.maxFrameSize;
|
|
801
|
-
break;
|
|
802
715
|
case 6:
|
|
803
716
|
if (this.profile.http2.maxHeaderListSize)
|
|
804
717
|
settings.maxHeaderListSize = this.profile.http2.maxHeaderListSize;
|
|
805
718
|
break;
|
|
806
719
|
}
|
|
807
720
|
});
|
|
808
|
-
this.
|
|
721
|
+
this.http2Client = http2.connect(`https://${this.hostname}`, {
|
|
809
722
|
createConnection: () => this.tlsSocket,
|
|
810
723
|
settings
|
|
811
724
|
});
|
|
812
|
-
this.
|
|
813
|
-
this.
|
|
814
|
-
this.
|
|
725
|
+
this.http2Client.once('connect', resolve);
|
|
726
|
+
this.http2Client.once('error', reject);
|
|
727
|
+
this.http2Client.once('timeout', () => reject(new Error('HTTP/2 session timeout')));
|
|
815
728
|
});
|
|
816
729
|
}
|
|
817
|
-
async request(path, options
|
|
818
|
-
|
|
819
|
-
|
|
730
|
+
async request(path, options, timingStart, headers, post_data, out, callback) {
|
|
731
|
+
const cleanHeaders = { ...headers };
|
|
732
|
+
delete cleanHeaders['connection'];
|
|
733
|
+
delete cleanHeaders['Connection'];
|
|
734
|
+
delete cleanHeaders['keep-alive'];
|
|
735
|
+
delete cleanHeaders['Keep-Alive'];
|
|
736
|
+
delete cleanHeaders['proxy-connection'];
|
|
737
|
+
delete cleanHeaders['Proxy-Connection'];
|
|
738
|
+
delete cleanHeaders['upgrade'];
|
|
739
|
+
delete cleanHeaders['Upgrade'];
|
|
740
|
+
delete cleanHeaders['transfer-encoding'];
|
|
741
|
+
delete cleanHeaders['Transfer-Encoding'];
|
|
742
|
+
if (this.negotiatedProtocol === 'http2' && this.http2Client && !this.http2Client.destroyed) {
|
|
743
|
+
await this.requestHttp2(path, options, timingStart, cleanHeaders, post_data, out, callback);
|
|
820
744
|
}
|
|
821
|
-
|
|
745
|
+
else {
|
|
746
|
+
await this.requestHttp1(path, options, timingStart, cleanHeaders, post_data, out, callback);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
async requestHttp2(path, options, timingStart, headers, post_data, out, callback) {
|
|
750
|
+
const reqHeaders = {
|
|
822
751
|
':method': options.method?.toUpperCase() || 'GET',
|
|
823
752
|
':authority': this.hostname,
|
|
824
753
|
':scheme': 'https',
|
|
825
|
-
':path': path
|
|
754
|
+
':path': path,
|
|
755
|
+
...headers
|
|
826
756
|
};
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
'accept-encoding': 'gzip, deflate, br, zstd',
|
|
835
|
-
'sec-fetch-site': this.profile.secFetchSite,
|
|
836
|
-
'sec-fetch-mode': this.profile.secFetchMode,
|
|
837
|
-
'sec-fetch-dest': this.profile.secFetchDest,
|
|
757
|
+
const stream = this.http2Client.request(reqHeaders);
|
|
758
|
+
const response = new events_1.EventEmitter();
|
|
759
|
+
response.timing = { socket: timingStart, lookup: 0, connect: 0, secureConnect: 0, response: 0, end: 0, total: 0 };
|
|
760
|
+
response.fingerprints = {
|
|
761
|
+
ja3: this.generateJA3(),
|
|
762
|
+
ja3Hash: crypto.createHash('md5').update(this.generateJA3()).digest('hex'),
|
|
763
|
+
akamai: `1:${this.profile.http2.headerTableSize};2:${this.profile.http2.enablePush};4:${this.profile.http2.initialWindowSize};6:${this.profile.http2.maxHeaderListSize ?? ''}|00|0|m,a,s,p`
|
|
838
764
|
};
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if (
|
|
857
|
-
headers['content-
|
|
858
|
-
|
|
859
|
-
|
|
765
|
+
const chunks = [];
|
|
766
|
+
const rawChunks = [];
|
|
767
|
+
let finished = false;
|
|
768
|
+
const finish = async (error) => {
|
|
769
|
+
if (finished)
|
|
770
|
+
return;
|
|
771
|
+
finished = true;
|
|
772
|
+
stream.removeAllListeners();
|
|
773
|
+
if (error) {
|
|
774
|
+
out.emit('error', error);
|
|
775
|
+
if (callback)
|
|
776
|
+
setImmediate(() => callback(error));
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
let body = Buffer.concat(chunks);
|
|
780
|
+
response.bytes = body.length;
|
|
781
|
+
response.raw = Buffer.concat(rawChunks);
|
|
782
|
+
if (options.decompress !== false && response.headers) {
|
|
783
|
+
const encoding = response.headers['content-encoding']?.toLowerCase();
|
|
784
|
+
if (encoding) {
|
|
785
|
+
try {
|
|
786
|
+
if (encoding.includes('gzip'))
|
|
787
|
+
body = await gunzipAsync(body);
|
|
788
|
+
else if (encoding.includes('br'))
|
|
789
|
+
body = await brotliDecompressAsync(body);
|
|
790
|
+
else if (encoding.includes('deflate'))
|
|
791
|
+
body = await inflateAsync(body);
|
|
792
|
+
}
|
|
793
|
+
catch { }
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
response.body = body;
|
|
797
|
+
try {
|
|
798
|
+
response.text = body.toString('utf-8');
|
|
799
|
+
if (response.headers['content-type']?.includes('application/json')) {
|
|
800
|
+
response.json = JSON.parse(response.text);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
catch { }
|
|
804
|
+
response.timing.end = Date.now() - timingStart;
|
|
805
|
+
response.timing.total = response.timing.end;
|
|
806
|
+
if (!out.writableEnded && !out.destroyed)
|
|
807
|
+
out.end();
|
|
808
|
+
if (callback)
|
|
809
|
+
setImmediate(() => callback(null, response, response.body));
|
|
810
|
+
};
|
|
811
|
+
stream.once('response', (hdrs) => {
|
|
812
|
+
response.statusCode = hdrs[':status'];
|
|
813
|
+
response.headers = hdrs;
|
|
814
|
+
if (hdrs['set-cookie'])
|
|
815
|
+
this.cookieJar.parseSetCookie(this.hostname, hdrs['set-cookie']);
|
|
816
|
+
response.timing.response = Date.now() - timingStart;
|
|
817
|
+
out.emit('header', response.statusCode, hdrs);
|
|
818
|
+
out.emit('headers', hdrs);
|
|
819
|
+
});
|
|
820
|
+
stream.on('data', (chunk) => {
|
|
821
|
+
if (finished || out.destroyed || out.writableEnded)
|
|
822
|
+
return;
|
|
823
|
+
const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
824
|
+
rawChunks.push(bufferChunk);
|
|
825
|
+
chunks.push(bufferChunk);
|
|
826
|
+
if (!out.write(bufferChunk))
|
|
827
|
+
stream.pause();
|
|
828
|
+
});
|
|
829
|
+
out.on('drain', () => stream.resume());
|
|
830
|
+
stream.once('end', finish);
|
|
831
|
+
stream.once('error', finish);
|
|
832
|
+
if (post_data) {
|
|
833
|
+
if (Buffer.isBuffer(post_data) || typeof post_data === 'string') {
|
|
834
|
+
if (!stream.closed && !stream.destroyed && stream.writable) {
|
|
835
|
+
try {
|
|
836
|
+
stream.write(post_data);
|
|
837
|
+
}
|
|
838
|
+
catch { }
|
|
839
|
+
}
|
|
840
|
+
setImmediate(() => {
|
|
841
|
+
if (!stream.closed && !stream.destroyed && stream.writable) {
|
|
842
|
+
stream.end();
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
else if (post_data instanceof stream_1.Readable) {
|
|
847
|
+
post_data.pipe(stream, { end: true });
|
|
848
|
+
post_data.on('error', err => stream.destroy(err));
|
|
860
849
|
}
|
|
861
|
-
else
|
|
862
|
-
|
|
850
|
+
else {
|
|
851
|
+
setImmediate(() => {
|
|
852
|
+
if (stream.writable)
|
|
853
|
+
stream.end();
|
|
854
|
+
});
|
|
863
855
|
}
|
|
864
856
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
857
|
+
else {
|
|
858
|
+
setImmediate(() => {
|
|
859
|
+
if (stream.writable)
|
|
860
|
+
stream.end();
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async requestHttp1(path, options, timingStart, headers, post_data, out, callback) {
|
|
865
|
+
const reqOptions = {
|
|
866
|
+
method: options.method?.toUpperCase() || 'GET',
|
|
867
|
+
path,
|
|
868
|
+
headers: { host: this.hostname, ...headers },
|
|
869
|
+
createConnection: () => this.tlsSocket
|
|
875
870
|
};
|
|
871
|
+
const req = require('https').request(reqOptions);
|
|
872
|
+
const response = new events_1.EventEmitter();
|
|
873
|
+
response.timing = { socket: timingStart, lookup: 0, connect: 0, secureConnect: 0, response: 0, end: 0, total: 0 };
|
|
876
874
|
response.fingerprints = {
|
|
877
875
|
ja3: this.generateJA3(),
|
|
878
876
|
ja3Hash: crypto.createHash('md5').update(this.generateJA3()).digest('hex'),
|
|
879
|
-
akamai:
|
|
877
|
+
akamai: ''
|
|
880
878
|
};
|
|
881
879
|
const chunks = [];
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
880
|
+
const rawChunks = [];
|
|
881
|
+
let finished = false;
|
|
882
|
+
const finish = async (error, res) => {
|
|
883
|
+
if (finished)
|
|
884
|
+
return;
|
|
885
|
+
finished = true;
|
|
886
|
+
req.removeAllListeners();
|
|
887
|
+
if (res)
|
|
888
|
+
res.removeAllListeners();
|
|
889
|
+
if (error) {
|
|
890
|
+
out.emit('error', error);
|
|
891
|
+
if (callback)
|
|
892
|
+
setImmediate(() => callback(error));
|
|
893
|
+
return;
|
|
889
894
|
}
|
|
890
|
-
response.timing.response = Date.now() - timingStart;
|
|
891
|
-
});
|
|
892
|
-
stream.on('data', (chunk) => chunks.push(chunk));
|
|
893
|
-
stream.once('end', async () => {
|
|
894
895
|
let body = Buffer.concat(chunks);
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
896
|
+
response.bytes = body.length;
|
|
897
|
+
response.raw = Buffer.concat(rawChunks);
|
|
898
|
+
if (options.decompress !== false && res?.headers) {
|
|
899
|
+
const encoding = (res.headers['content-encoding'] || '').toLowerCase();
|
|
900
|
+
if (encoding) {
|
|
901
|
+
try {
|
|
902
|
+
if (encoding.includes('gzip'))
|
|
903
|
+
body = await gunzipAsync(body);
|
|
904
|
+
else if (encoding.includes('br'))
|
|
905
|
+
body = await brotliDecompressAsync(body);
|
|
906
|
+
else if (encoding.includes('deflate'))
|
|
907
|
+
body = await inflateAsync(body);
|
|
908
|
+
}
|
|
909
|
+
catch { }
|
|
904
910
|
}
|
|
905
|
-
catch (e) { }
|
|
906
911
|
}
|
|
907
912
|
response.body = body;
|
|
908
913
|
try {
|
|
909
914
|
response.text = body.toString('utf-8');
|
|
910
|
-
if (
|
|
915
|
+
if (res.headers['content-type']?.includes('application/json')) {
|
|
911
916
|
response.json = JSON.parse(response.text);
|
|
912
917
|
}
|
|
913
918
|
}
|
|
914
919
|
catch { }
|
|
915
920
|
response.timing.end = Date.now() - timingStart;
|
|
916
921
|
response.timing.total = response.timing.end;
|
|
917
|
-
|
|
922
|
+
if (!out.writableEnded && !out.destroyed)
|
|
923
|
+
out.end();
|
|
924
|
+
if (callback)
|
|
925
|
+
setImmediate(() => callback(null, response, response.body));
|
|
926
|
+
};
|
|
927
|
+
req.once('response', (res) => {
|
|
928
|
+
response.statusCode = res.statusCode;
|
|
929
|
+
response.headers = res.headers;
|
|
930
|
+
if (res.headers['set-cookie'])
|
|
931
|
+
this.cookieJar.parseSetCookie(this.hostname, res.headers['set-cookie']);
|
|
932
|
+
response.timing.response = Date.now() - timingStart;
|
|
933
|
+
out.emit('header', response.statusCode, res.headers);
|
|
934
|
+
out.emit('headers', res.headers);
|
|
935
|
+
res.on('data', (chunk) => {
|
|
936
|
+
if (finished || out.destroyed || out.writableEnded)
|
|
937
|
+
return;
|
|
938
|
+
const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
939
|
+
rawChunks.push(bufferChunk);
|
|
940
|
+
chunks.push(bufferChunk);
|
|
941
|
+
out.write(bufferChunk);
|
|
942
|
+
});
|
|
943
|
+
res.once('end', () => finish(null, res));
|
|
944
|
+
res.once('error', finish);
|
|
918
945
|
});
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
946
|
+
req.once('error', finish);
|
|
947
|
+
if (post_data) {
|
|
948
|
+
if (Buffer.isBuffer(post_data) || typeof post_data === 'string') {
|
|
949
|
+
req.write(post_data);
|
|
950
|
+
req.end();
|
|
924
951
|
}
|
|
925
|
-
else if (
|
|
926
|
-
|
|
952
|
+
else if (post_data instanceof stream_1.Readable) {
|
|
953
|
+
post_data.pipe(req);
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
req.end();
|
|
927
957
|
}
|
|
928
958
|
}
|
|
929
959
|
else {
|
|
930
|
-
|
|
960
|
+
req.end();
|
|
931
961
|
}
|
|
932
|
-
return new Promise((resolve, reject) => {
|
|
933
|
-
response.once('complete', () => resolve(response));
|
|
934
|
-
response.once('error', reject);
|
|
935
|
-
});
|
|
936
962
|
}
|
|
937
963
|
generateJA3() {
|
|
938
964
|
const version = '771';
|
|
939
|
-
const ciphers = this.profile.tls.cipherSuites
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
const extensions = this.profile.tls.extensions
|
|
943
|
-
.filter(e => typeof e === 'number' && e < 0xff00)
|
|
944
|
-
.join('-');
|
|
945
|
-
const curves = this.profile.tls.supportedGroups
|
|
946
|
-
.filter(g => g < 0xff00)
|
|
947
|
-
.join('-');
|
|
965
|
+
const ciphers = this.profile.tls.cipherSuites.filter((c) => c < 0xff00).join('-');
|
|
966
|
+
const extensions = this.profile.tls.extensions.filter((e) => typeof e === 'number' && e < 0xff00).join('-');
|
|
967
|
+
const curves = this.profile.tls.supportedGroups.filter((g) => g < 0xff00).join('-');
|
|
948
968
|
const ecPoints = this.profile.tls.ecPointFormats.join('-');
|
|
949
969
|
return `${version},${ciphers},${extensions},${curves},${ecPoints}`;
|
|
950
970
|
}
|
|
951
971
|
destroy() {
|
|
952
|
-
if (this.
|
|
953
|
-
this.
|
|
954
|
-
|
|
955
|
-
|
|
972
|
+
if (this.http2Client && !this.http2Client.destroyed)
|
|
973
|
+
this.http2Client.destroy();
|
|
974
|
+
this.http2Client = null;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
class TLSSocketManager extends events_1.EventEmitter {
|
|
978
|
+
constructor(profile, proxy) {
|
|
979
|
+
super();
|
|
980
|
+
this.socket = null;
|
|
981
|
+
this.tlsSocket = null;
|
|
982
|
+
this.startTime = 0;
|
|
983
|
+
this.hostname = '';
|
|
984
|
+
this.port = 443;
|
|
985
|
+
this.profile = profile;
|
|
986
|
+
this.proxy = proxy;
|
|
987
|
+
this.timing = { socket: 0, lookup: 0, connect: 0, secureConnect: 0, response: 0, end: 0, total: 0 };
|
|
988
|
+
}
|
|
989
|
+
async connect(hostname, port = 443) {
|
|
990
|
+
this.hostname = hostname;
|
|
991
|
+
this.port = port;
|
|
992
|
+
this.startTime = Date.now();
|
|
993
|
+
return new Promise((resolve, reject) => {
|
|
994
|
+
const timeout = setTimeout(() => {
|
|
995
|
+
this.destroy();
|
|
996
|
+
reject(new Error('Connection timeout'));
|
|
997
|
+
}, 30000);
|
|
998
|
+
const handleError = (err) => {
|
|
999
|
+
clearTimeout(timeout);
|
|
1000
|
+
this.destroy();
|
|
1001
|
+
reject(err);
|
|
1002
|
+
};
|
|
1003
|
+
if (this.proxy) {
|
|
1004
|
+
const proxyUrl = new url.URL(this.proxy);
|
|
1005
|
+
this.socket = net.connect(+proxyUrl.port || 80, proxyUrl.hostname, () => {
|
|
1006
|
+
this.timing.connect = Date.now() - this.startTime;
|
|
1007
|
+
let request = `CONNECT ${hostname}:${port} HTTP/1.1\r\nHost: ${hostname}:${port}\r\n`;
|
|
1008
|
+
if (proxyUrl.username && proxyUrl.password) {
|
|
1009
|
+
const auth = Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString('base64');
|
|
1010
|
+
request += `Proxy-Authorization: Basic ${auth}\r\n`;
|
|
1011
|
+
}
|
|
1012
|
+
request += '\r\n';
|
|
1013
|
+
this.socket.write(request);
|
|
1014
|
+
});
|
|
1015
|
+
let response = '';
|
|
1016
|
+
const onData = (chunk) => {
|
|
1017
|
+
response += chunk.toString();
|
|
1018
|
+
if (response.includes('\r\n\r\n')) {
|
|
1019
|
+
this.socket.removeListener('data', onData);
|
|
1020
|
+
if (!response.startsWith('HTTP/1.1 200')) {
|
|
1021
|
+
clearTimeout(timeout);
|
|
1022
|
+
this.destroy();
|
|
1023
|
+
return reject(new Error('Proxy failed: ' + response.split('\r\n')[0]));
|
|
1024
|
+
}
|
|
1025
|
+
this.proceedWithTLS(resolve, reject, timeout);
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
this.socket.on('data', onData);
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
this.socket = new net.Socket();
|
|
1032
|
+
this.socket.setNoDelay(true);
|
|
1033
|
+
this.socket.setKeepAlive(true, 60000);
|
|
1034
|
+
this.socket.once('lookup', () => {
|
|
1035
|
+
this.timing.lookup = Date.now() - this.startTime;
|
|
1036
|
+
});
|
|
1037
|
+
this.socket.connect(port, hostname, () => {
|
|
1038
|
+
this.timing.connect = Date.now() - this.startTime;
|
|
1039
|
+
this.proceedWithTLS(resolve, reject, timeout);
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
this.socket.on('error', handleError);
|
|
1043
|
+
this.socket.on('timeout', () => handleError(new Error('Socket timeout')));
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
proceedWithTLS(resolve, reject, timeout) {
|
|
1047
|
+
const validCiphers = [
|
|
1048
|
+
'TLS_AES_256_GCM_SHA384',
|
|
1049
|
+
'TLS_CHACHA20_POLY1305_SHA256',
|
|
1050
|
+
'TLS_AES_128_GCM_SHA256',
|
|
1051
|
+
'ECDHE-ECDSA-AES256-GCM-SHA384',
|
|
1052
|
+
'ECDHE-RSA-AES256-GCM-SHA384',
|
|
1053
|
+
'ECDHE-ECDSA-CHACHA20-POLY1305',
|
|
1054
|
+
'ECDHE-RSA-CHACHA20-POLY1305',
|
|
1055
|
+
'ECDHE-ECDSA-AES128-GCM-SHA256',
|
|
1056
|
+
'ECDHE-RSA-AES128-GCM-SHA256',
|
|
1057
|
+
'ECDHE-ECDSA-AES256-SHA',
|
|
1058
|
+
'ECDHE-RSA-AES256-SHA',
|
|
1059
|
+
'ECDHE-ECDSA-AES128-SHA',
|
|
1060
|
+
'ECDHE-RSA-AES128-SHA',
|
|
1061
|
+
'AES256-GCM-SHA384',
|
|
1062
|
+
'AES128-GCM-SHA256',
|
|
1063
|
+
'AES256-SHA',
|
|
1064
|
+
'AES128-SHA'
|
|
1065
|
+
].join(':');
|
|
1066
|
+
const tlsOptions = {
|
|
1067
|
+
socket: this.socket,
|
|
1068
|
+
servername: this.hostname,
|
|
1069
|
+
ALPNProtocols: this.profile.tls.alpnProtocols,
|
|
1070
|
+
ciphers: validCiphers,
|
|
1071
|
+
minVersion: 'TLSv1.2',
|
|
1072
|
+
maxVersion: 'TLSv1.3',
|
|
1073
|
+
rejectUnauthorized: false,
|
|
1074
|
+
requestCert: false,
|
|
1075
|
+
honorCipherOrder: true,
|
|
1076
|
+
sessionTimeout: 300
|
|
1077
|
+
};
|
|
1078
|
+
this.tlsSocket = tls.connect(tlsOptions);
|
|
1079
|
+
this.tlsSocket.once('secureConnect', () => {
|
|
1080
|
+
clearTimeout(timeout);
|
|
1081
|
+
this.timing.secureConnect = Date.now() - this.startTime;
|
|
1082
|
+
this.timing.socket = this.startTime;
|
|
1083
|
+
resolve(this.tlsSocket);
|
|
1084
|
+
});
|
|
1085
|
+
this.tlsSocket.on('error', (err) => {
|
|
1086
|
+
clearTimeout(timeout);
|
|
1087
|
+
this.destroy();
|
|
1088
|
+
reject(err);
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
getTiming() {
|
|
1092
|
+
return { ...this.timing, total: Date.now() - this.startTime };
|
|
1093
|
+
}
|
|
1094
|
+
destroy() {
|
|
1095
|
+
this.tlsSocket?.destroy();
|
|
1096
|
+
this.socket?.destroy();
|
|
1097
|
+
this.tlsSocket = null;
|
|
1098
|
+
this.socket = null;
|
|
1099
|
+
}
|
|
1100
|
+
getSocket() {
|
|
1101
|
+
return this.tlsSocket;
|
|
956
1102
|
}
|
|
957
1103
|
}
|
|
958
1104
|
class AdvancedTLSClient {
|
|
@@ -961,74 +1107,280 @@ class AdvancedTLSClient {
|
|
|
961
1107
|
this.cookieJar = new CookieJar();
|
|
962
1108
|
this.maxCachedSessions = 10;
|
|
963
1109
|
this.sessionTimeout = 300000;
|
|
1110
|
+
this.defaults = {
|
|
1111
|
+
boundary: '--------------------SIPUTZXCOMPANY',
|
|
1112
|
+
encoding: 'utf8',
|
|
1113
|
+
parse_response: 'all',
|
|
1114
|
+
proxy: null,
|
|
1115
|
+
agent: null,
|
|
1116
|
+
headers: {},
|
|
1117
|
+
accept: '*/*',
|
|
1118
|
+
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
|
1119
|
+
open_timeout: 10000,
|
|
1120
|
+
response_timeout: 0,
|
|
1121
|
+
read_timeout: 0,
|
|
1122
|
+
follow_max: 0,
|
|
1123
|
+
stream_length: -1,
|
|
1124
|
+
signal: null,
|
|
1125
|
+
compressed: false,
|
|
1126
|
+
decode_response: true,
|
|
1127
|
+
parse_cookies: true,
|
|
1128
|
+
follow_set_cookies: false,
|
|
1129
|
+
follow_set_referer: false,
|
|
1130
|
+
follow_keep_method: false,
|
|
1131
|
+
follow_if_same_host: false,
|
|
1132
|
+
follow_if_same_protocol: false,
|
|
1133
|
+
follow_if_same_location: false,
|
|
1134
|
+
use_proxy_from_env_var: true
|
|
1135
|
+
};
|
|
964
1136
|
this.profile = ProfileRegistry.get(profileName || 'chrome_133');
|
|
965
1137
|
this.cleanupInterval = setInterval(() => {
|
|
966
1138
|
const now = Date.now();
|
|
967
1139
|
for (const [key, session] of this.sessionCache) {
|
|
968
1140
|
if (now - session.lastUsed > this.sessionTimeout) {
|
|
969
|
-
session.
|
|
1141
|
+
session.clientManager.destroy();
|
|
970
1142
|
session.tlsManager.destroy();
|
|
971
1143
|
this.sessionCache.delete(key);
|
|
972
1144
|
}
|
|
973
1145
|
}
|
|
974
1146
|
}, 60000);
|
|
975
1147
|
}
|
|
976
|
-
|
|
977
|
-
const
|
|
1148
|
+
setup(uri, options) {
|
|
1149
|
+
const config = {
|
|
1150
|
+
headers: { ...options.headers },
|
|
1151
|
+
proxy: options.proxy || this.defaults.proxy,
|
|
1152
|
+
decompress: options.decompress !== undefined ? options.decompress : true
|
|
1153
|
+
};
|
|
1154
|
+
config.headers['user-agent'] = this.profile.userAgent;
|
|
1155
|
+
config.headers['accept-language'] = this.profile.acceptLanguage || 'en-US,en;q=0.9';
|
|
1156
|
+
config.headers['accept-encoding'] = 'gzip, deflate, br';
|
|
1157
|
+
config.headers['accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8';
|
|
1158
|
+
config.headers['sec-ch-ua'] = this.profile.secChUa;
|
|
1159
|
+
config.headers['sec-ch-ua-mobile'] = this.profile.secChUaMobile;
|
|
1160
|
+
config.headers['sec-ch-ua-platform'] = this.profile.secChUaPlatform;
|
|
1161
|
+
config.headers['sec-fetch-site'] = this.profile.secFetchSite;
|
|
1162
|
+
config.headers['sec-fetch-mode'] = this.profile.secFetchMode;
|
|
1163
|
+
config.headers['sec-fetch-dest'] = this.profile.secFetchDest;
|
|
1164
|
+
if (this.profile.upgradeInsecureRequests)
|
|
1165
|
+
config.headers['upgrade-insecure-requests'] = this.profile.upgradeInsecureRequests;
|
|
1166
|
+
if (this.profile.priority)
|
|
1167
|
+
config.headers['priority'] = this.profile.priority;
|
|
1168
|
+
return config;
|
|
1169
|
+
}
|
|
1170
|
+
async request(uri, data, options = {}, callback) {
|
|
1171
|
+
if (typeof options === 'function') {
|
|
1172
|
+
callback = options;
|
|
1173
|
+
options = {};
|
|
1174
|
+
}
|
|
1175
|
+
const parsed = new url.URL(uri);
|
|
978
1176
|
const hostname = parsed.hostname;
|
|
979
1177
|
const port = parsed.port ? +parsed.port : 443;
|
|
980
1178
|
const path = parsed.pathname + parsed.search;
|
|
981
1179
|
const cacheKey = `${hostname}:${port}`;
|
|
982
|
-
let session = this.sessionCache.get(cacheKey);
|
|
983
1180
|
const startTime = Date.now();
|
|
1181
|
+
const out = new stream_1.PassThrough();
|
|
1182
|
+
let session = this.sessionCache.get(cacheKey);
|
|
984
1183
|
if (!session || session.tlsManager.getSocket()?.destroyed) {
|
|
985
|
-
const tlsManager = new TLSSocketManager(this.profile);
|
|
1184
|
+
const tlsManager = new TLSSocketManager(this.profile, options.proxy);
|
|
986
1185
|
const tlsSocket = await tlsManager.connect(hostname, port);
|
|
987
|
-
const
|
|
988
|
-
await
|
|
989
|
-
session = { tlsManager,
|
|
1186
|
+
const clientManager = new UnifiedClientManager(tlsSocket, this.profile, hostname, this.cookieJar);
|
|
1187
|
+
await clientManager.initialize();
|
|
1188
|
+
session = { tlsManager, clientManager, lastUsed: Date.now() };
|
|
990
1189
|
this.sessionCache.set(cacheKey, session);
|
|
991
1190
|
if (this.sessionCache.size > this.maxCachedSessions) {
|
|
992
1191
|
const oldest = [...this.sessionCache.entries()].sort((a, b) => a[1].lastUsed - b[1].lastUsed)[0];
|
|
993
|
-
oldest[1].
|
|
1192
|
+
oldest[1].clientManager.destroy();
|
|
994
1193
|
oldest[1].tlsManager.destroy();
|
|
995
1194
|
this.sessionCache.delete(oldest[0]);
|
|
996
1195
|
}
|
|
997
1196
|
}
|
|
998
1197
|
session.lastUsed = Date.now();
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1198
|
+
const config = this.setup(uri, options);
|
|
1199
|
+
let post_data = null;
|
|
1200
|
+
let json = options.json || (options.json !== false && config.headers['content-type']?.includes('application/json'));
|
|
1201
|
+
if (data) {
|
|
1202
|
+
if (options.multipart) {
|
|
1203
|
+
const boundary = options.boundary || this.defaults.boundary;
|
|
1204
|
+
post_data = await this.buildMultipart(data, boundary);
|
|
1205
|
+
config.headers['content-type'] = `multipart/form-data; boundary=${boundary}`;
|
|
1206
|
+
}
|
|
1207
|
+
else if (data instanceof stream_1.Readable) {
|
|
1208
|
+
post_data = data;
|
|
1209
|
+
}
|
|
1210
|
+
else if (Buffer.isBuffer(data)) {
|
|
1211
|
+
post_data = data;
|
|
1212
|
+
}
|
|
1213
|
+
else if (typeof data === 'string') {
|
|
1214
|
+
post_data = data;
|
|
1215
|
+
}
|
|
1216
|
+
else if (json) {
|
|
1217
|
+
post_data = JSON.stringify(data);
|
|
1218
|
+
config.headers['content-type'] = 'application/json; charset=utf-8';
|
|
1219
|
+
}
|
|
1220
|
+
else {
|
|
1221
|
+
post_data = new URLSearchParams(data).toString();
|
|
1222
|
+
config.headers['content-type'] = 'application/x-www-form-urlencoded';
|
|
1223
|
+
}
|
|
1224
|
+
if (post_data && (typeof post_data === 'string' || Buffer.isBuffer(post_data))) {
|
|
1225
|
+
config.headers['content-length'] = Buffer.byteLength(post_data).toString();
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1014
1228
|
const cookies = this.cookieJar.getCookies(hostname, parsed.pathname, parsed.protocol === 'https:');
|
|
1015
1229
|
if (cookies)
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1230
|
+
config.headers['cookie'] = cookies;
|
|
1231
|
+
if (options.stream === true) {
|
|
1232
|
+
session.clientManager.request(path, options, startTime, config.headers, post_data, out);
|
|
1233
|
+
return out;
|
|
1234
|
+
}
|
|
1235
|
+
if (callback) {
|
|
1236
|
+
const typedCallback = callback;
|
|
1237
|
+
session.clientManager.request(path, options, startTime, config.headers, post_data, out, typedCallback).catch(err => {
|
|
1238
|
+
if (typedCallback)
|
|
1239
|
+
typedCallback(err);
|
|
1240
|
+
});
|
|
1241
|
+
return out;
|
|
1242
|
+
}
|
|
1243
|
+
return new Promise((resolve, reject) => {
|
|
1244
|
+
let statusCode = 0;
|
|
1245
|
+
let headers = {};
|
|
1246
|
+
const chunks = [];
|
|
1247
|
+
let done = false;
|
|
1248
|
+
const complete = async (err) => {
|
|
1249
|
+
if (done)
|
|
1250
|
+
return;
|
|
1251
|
+
done = true;
|
|
1252
|
+
if (err)
|
|
1253
|
+
return reject(err);
|
|
1254
|
+
let body = Buffer.concat(chunks);
|
|
1255
|
+
if (options.decompress !== false && headers) {
|
|
1256
|
+
const encoding = headers['content-encoding']?.toLowerCase();
|
|
1257
|
+
if (encoding) {
|
|
1258
|
+
try {
|
|
1259
|
+
if (encoding.includes('gzip'))
|
|
1260
|
+
body = await gunzipAsync(body);
|
|
1261
|
+
else if (encoding.includes('br'))
|
|
1262
|
+
body = await brotliDecompressAsync(body);
|
|
1263
|
+
else if (encoding.includes('deflate'))
|
|
1264
|
+
body = await inflateAsync(body);
|
|
1265
|
+
}
|
|
1266
|
+
catch { }
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
const text = body.toString('utf-8');
|
|
1270
|
+
let parsedJson;
|
|
1271
|
+
const ct = headers['content-type']?.toLowerCase();
|
|
1272
|
+
if (ct?.includes('application/json')) {
|
|
1273
|
+
try {
|
|
1274
|
+
parsedJson = JSON.parse(text);
|
|
1275
|
+
}
|
|
1276
|
+
catch { }
|
|
1277
|
+
}
|
|
1278
|
+
resolve({
|
|
1279
|
+
statusCode,
|
|
1280
|
+
headers,
|
|
1281
|
+
body,
|
|
1282
|
+
text,
|
|
1283
|
+
json: parsedJson,
|
|
1284
|
+
fingerprints: {
|
|
1285
|
+
ja3: this.generateJA3(),
|
|
1286
|
+
ja3Hash: crypto.createHash('md5').update(this.generateJA3()).digest('hex'),
|
|
1287
|
+
akamai: `1:${this.profile.http2.headerTableSize};2:${this.profile.http2.enablePush};4:${this.profile.http2.initialWindowSize};6:${this.profile.http2.maxHeaderListSize ?? ''}|00|0|m,a,s,p`
|
|
1288
|
+
},
|
|
1289
|
+
timing: {
|
|
1290
|
+
socket: startTime,
|
|
1291
|
+
lookup: 0,
|
|
1292
|
+
connect: 0,
|
|
1293
|
+
secureConnect: 0,
|
|
1294
|
+
response: Date.now() - startTime,
|
|
1295
|
+
end: Date.now() - startTime,
|
|
1296
|
+
total: Date.now() - startTime
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
1300
|
+
out.on('header', (status, hdrs) => {
|
|
1301
|
+
statusCode = status;
|
|
1302
|
+
headers = hdrs;
|
|
1303
|
+
});
|
|
1304
|
+
out.on('data', (chunk) => {
|
|
1305
|
+
if (!done)
|
|
1306
|
+
chunks.push(chunk);
|
|
1307
|
+
});
|
|
1308
|
+
out.once('end', () => complete());
|
|
1309
|
+
out.once('error', complete);
|
|
1310
|
+
session.clientManager.request(path, options, startTime, config.headers, post_data, out).catch(complete);
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
generateJA3() {
|
|
1314
|
+
const version = '771';
|
|
1315
|
+
const ciphers = this.profile.tls.cipherSuites.filter((c) => c < 0xff00).join('-');
|
|
1316
|
+
const extensions = this.profile.tls.extensions.filter((e) => typeof e === 'number' && e < 0xff00).join('-');
|
|
1317
|
+
const curves = this.profile.tls.supportedGroups.filter((g) => g < 0xff00).join('-');
|
|
1318
|
+
const ecPoints = this.profile.tls.ecPointFormats.join('-');
|
|
1319
|
+
return `${version},${ciphers},${extensions},${curves},${ecPoints}`;
|
|
1320
|
+
}
|
|
1321
|
+
async buildMultipart(data, boundary) {
|
|
1322
|
+
return new Promise((resolve, reject) => {
|
|
1323
|
+
let body = '';
|
|
1324
|
+
const object = this.flatten(data);
|
|
1325
|
+
const count = Object.keys(object).length;
|
|
1326
|
+
if (count === 0)
|
|
1327
|
+
return reject(new Error('Empty multipart body'));
|
|
1328
|
+
let doneCount = count;
|
|
1329
|
+
const done = () => {
|
|
1330
|
+
if (--doneCount === 0)
|
|
1331
|
+
resolve(Buffer.from(body + '--' + boundary + '--\r\n'));
|
|
1332
|
+
};
|
|
1333
|
+
for (const key in object) {
|
|
1334
|
+
const value = object[key];
|
|
1335
|
+
if (value === null || typeof value === 'undefined') {
|
|
1336
|
+
done();
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const part = Buffer.isBuffer(value) ? { buffer: value, content_type: 'application/octet-stream' } : (value.buffer || value.file || value.content_type) ? value : { value };
|
|
1340
|
+
this.generateMultipart(key, part, boundary).then(section => { body += section; done(); });
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
async generateMultipart(name, part, boundary) {
|
|
1345
|
+
let return_part = `--${boundary}\r\n`;
|
|
1346
|
+
return_part += `Content-Disposition: form-data; name="${name}"`;
|
|
1347
|
+
const append = (data, filename) => {
|
|
1348
|
+
if (data) {
|
|
1349
|
+
return_part += `; filename="${encodeURIComponent(filename)}"\r\n`;
|
|
1350
|
+
return_part += `Content-Type: ${part.content_type || 'application/octet-stream'}\r\n\r\n`;
|
|
1351
|
+
return_part += data.toString('binary');
|
|
1352
|
+
}
|
|
1353
|
+
return return_part + '\r\n';
|
|
1354
|
+
};
|
|
1355
|
+
if ((part.file || part.buffer) && part.content_type) {
|
|
1356
|
+
const filename = part.filename || (part.file ? path.basename(part.file) : name);
|
|
1357
|
+
if (part.buffer)
|
|
1358
|
+
return append(part.buffer, filename);
|
|
1359
|
+
const data = await fs.promises.readFile(part.file);
|
|
1360
|
+
return append(data, filename);
|
|
1361
|
+
}
|
|
1362
|
+
else {
|
|
1363
|
+
return_part += '\r\n\r\n' + String(part.value || '') + '\r\n';
|
|
1364
|
+
return return_part;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
flatten(object, into = {}, prefix) {
|
|
1368
|
+
for (const key in object) {
|
|
1369
|
+
const prefix_key = prefix ? `${prefix}[${key}]` : key;
|
|
1370
|
+
const prop = object[key];
|
|
1371
|
+
if (prop && typeof prop === 'object' && !(prop.buffer || prop.file || prop.content_type)) {
|
|
1372
|
+
this.flatten(prop, into, prefix_key);
|
|
1373
|
+
}
|
|
1374
|
+
else {
|
|
1375
|
+
into[prefix_key] = prop;
|
|
1024
1376
|
}
|
|
1025
1377
|
}
|
|
1026
|
-
return
|
|
1378
|
+
return into;
|
|
1027
1379
|
}
|
|
1028
1380
|
destroy() {
|
|
1029
1381
|
clearInterval(this.cleanupInterval);
|
|
1030
1382
|
this.sessionCache.forEach(s => {
|
|
1031
|
-
s.
|
|
1383
|
+
s.clientManager.destroy();
|
|
1032
1384
|
s.tlsManager.destroy();
|
|
1033
1385
|
});
|
|
1034
1386
|
this.sessionCache.clear();
|