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