advanced-tls-client 3.0.1 → 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 +2 -2
- package/dist/index.js +214 -207
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -139,7 +139,7 @@ declare class AdvancedTLSClient {
|
|
|
139
139
|
private cleanupInterval;
|
|
140
140
|
private defaults;
|
|
141
141
|
constructor(profileName?: string);
|
|
142
|
-
setup
|
|
142
|
+
private setup;
|
|
143
143
|
request(uri: string, data?: any, options?: RequestOptions, callback?: Function): Promise<FullResponse | PassThrough>;
|
|
144
144
|
private generateJA3;
|
|
145
145
|
private buildMultipart;
|
|
@@ -147,4 +147,4 @@ declare class AdvancedTLSClient {
|
|
|
147
147
|
private flatten;
|
|
148
148
|
destroy(): void;
|
|
149
149
|
}
|
|
150
|
-
export { AdvancedTLSClient, ProfileRegistry
|
|
150
|
+
export { AdvancedTLSClient, ProfileRegistry };
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,6 @@ 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"));
|
|
40
39
|
const http2 = __importStar(require("http2"));
|
|
41
40
|
const crypto = __importStar(require("crypto"));
|
|
42
41
|
const events_1 = require("events");
|
|
@@ -46,9 +45,9 @@ const stream_1 = require("stream");
|
|
|
46
45
|
const fs = __importStar(require("fs"));
|
|
47
46
|
const path = __importStar(require("path"));
|
|
48
47
|
const url = __importStar(require("url"));
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
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);
|
|
52
51
|
const GREASE_VALUES = [0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a, 0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa];
|
|
53
52
|
class ProfileRegistry {
|
|
54
53
|
static getGrease() {
|
|
@@ -675,16 +674,15 @@ class CookieJar {
|
|
|
675
674
|
}
|
|
676
675
|
}
|
|
677
676
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
})(Protocol || (Protocol = {}));
|
|
677
|
+
const Protocol = {
|
|
678
|
+
HTTP1: 'http1',
|
|
679
|
+
HTTP2: 'http2'
|
|
680
|
+
};
|
|
683
681
|
class UnifiedClientManager extends events_1.EventEmitter {
|
|
684
682
|
constructor(tlsSocket, profile, hostname, cookieJar) {
|
|
685
683
|
super();
|
|
686
684
|
this.http2Client = null;
|
|
687
|
-
this.negotiatedProtocol =
|
|
685
|
+
this.negotiatedProtocol = 'http1';
|
|
688
686
|
this.tlsSocket = tlsSocket;
|
|
689
687
|
this.profile = profile;
|
|
690
688
|
this.hostname = hostname;
|
|
@@ -693,17 +691,17 @@ class UnifiedClientManager extends events_1.EventEmitter {
|
|
|
693
691
|
async initialize() {
|
|
694
692
|
const alpn = this.tlsSocket.alpnProtocol;
|
|
695
693
|
if (alpn === 'h2') {
|
|
696
|
-
this.negotiatedProtocol =
|
|
694
|
+
this.negotiatedProtocol = 'http2';
|
|
697
695
|
await this.createHttp2Session();
|
|
698
696
|
}
|
|
699
697
|
else {
|
|
700
|
-
this.negotiatedProtocol =
|
|
698
|
+
this.negotiatedProtocol = 'http1';
|
|
701
699
|
}
|
|
702
700
|
}
|
|
703
701
|
async createHttp2Session() {
|
|
704
702
|
return new Promise((resolve, reject) => {
|
|
705
703
|
const settings = {};
|
|
706
|
-
this.profile.http2.settingsOrder.forEach(id => {
|
|
704
|
+
this.profile.http2.settingsOrder.forEach((id) => {
|
|
707
705
|
switch (id) {
|
|
708
706
|
case 1:
|
|
709
707
|
settings.headerTableSize = this.profile.http2.headerTableSize;
|
|
@@ -711,17 +709,9 @@ class UnifiedClientManager extends events_1.EventEmitter {
|
|
|
711
709
|
case 2:
|
|
712
710
|
settings.enablePush = this.profile.http2.enablePush === 1;
|
|
713
711
|
break;
|
|
714
|
-
case 3:
|
|
715
|
-
if (this.profile.http2.maxConcurrentStreams)
|
|
716
|
-
settings.maxConcurrentStreams = this.profile.http2.maxConcurrentStreams;
|
|
717
|
-
break;
|
|
718
712
|
case 4:
|
|
719
713
|
settings.initialWindowSize = this.profile.http2.initialWindowSize;
|
|
720
714
|
break;
|
|
721
|
-
case 5:
|
|
722
|
-
if (this.profile.http2.maxFrameSize)
|
|
723
|
-
settings.maxFrameSize = this.profile.http2.maxFrameSize;
|
|
724
|
-
break;
|
|
725
715
|
case 6:
|
|
726
716
|
if (this.profile.http2.maxHeaderListSize)
|
|
727
717
|
settings.maxHeaderListSize = this.profile.http2.maxHeaderListSize;
|
|
@@ -732,7 +722,7 @@ class UnifiedClientManager extends events_1.EventEmitter {
|
|
|
732
722
|
createConnection: () => this.tlsSocket,
|
|
733
723
|
settings
|
|
734
724
|
});
|
|
735
|
-
this.http2Client.once('connect',
|
|
725
|
+
this.http2Client.once('connect', resolve);
|
|
736
726
|
this.http2Client.once('error', reject);
|
|
737
727
|
this.http2Client.once('timeout', () => reject(new Error('HTTP/2 session timeout')));
|
|
738
728
|
});
|
|
@@ -749,7 +739,7 @@ class UnifiedClientManager extends events_1.EventEmitter {
|
|
|
749
739
|
delete cleanHeaders['Upgrade'];
|
|
750
740
|
delete cleanHeaders['transfer-encoding'];
|
|
751
741
|
delete cleanHeaders['Transfer-Encoding'];
|
|
752
|
-
if (this.negotiatedProtocol ===
|
|
742
|
+
if (this.negotiatedProtocol === 'http2' && this.http2Client && !this.http2Client.destroyed) {
|
|
753
743
|
await this.requestHttp2(path, options, timingStart, cleanHeaders, post_data, out, callback);
|
|
754
744
|
}
|
|
755
745
|
else {
|
|
@@ -757,8 +747,6 @@ class UnifiedClientManager extends events_1.EventEmitter {
|
|
|
757
747
|
}
|
|
758
748
|
}
|
|
759
749
|
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
750
|
const reqHeaders = {
|
|
763
751
|
':method': options.method?.toUpperCase() || 'GET',
|
|
764
752
|
':authority': this.hostname,
|
|
@@ -774,87 +762,113 @@ class UnifiedClientManager extends events_1.EventEmitter {
|
|
|
774
762
|
ja3Hash: crypto.createHash('md5').update(this.generateJA3()).digest('hex'),
|
|
775
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`
|
|
776
764
|
};
|
|
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']);
|
|
782
|
-
response.timing.response = Date.now() - timingStart;
|
|
783
|
-
out.emit('header', response.statusCode, hdrs);
|
|
784
|
-
out.emit('headers', hdrs);
|
|
785
|
-
});
|
|
786
765
|
const chunks = [];
|
|
787
766
|
const rawChunks = [];
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
+
}
|
|
796
779
|
let body = Buffer.concat(chunks);
|
|
797
780
|
response.bytes = body.length;
|
|
798
781
|
response.raw = Buffer.concat(rawChunks);
|
|
799
782
|
if (options.decompress !== false && response.headers) {
|
|
800
|
-
const
|
|
801
|
-
const encoding = Array.isArray(encodingHeader) ? encodingHeader[0]?.toLowerCase() : typeof encodingHeader === 'string' ? encodingHeader.toLowerCase() : undefined;
|
|
783
|
+
const encoding = response.headers['content-encoding']?.toLowerCase();
|
|
802
784
|
if (encoding) {
|
|
803
785
|
try {
|
|
804
786
|
if (encoding.includes('gzip'))
|
|
805
|
-
body = await
|
|
787
|
+
body = await gunzipAsync(body);
|
|
806
788
|
else if (encoding.includes('br'))
|
|
807
|
-
body = await
|
|
789
|
+
body = await brotliDecompressAsync(body);
|
|
808
790
|
else if (encoding.includes('deflate'))
|
|
809
|
-
body = await
|
|
810
|
-
}
|
|
811
|
-
catch (e) {
|
|
812
|
-
console.error('Decompression error:', e);
|
|
791
|
+
body = await inflateAsync(body);
|
|
813
792
|
}
|
|
793
|
+
catch { }
|
|
814
794
|
}
|
|
815
795
|
}
|
|
816
796
|
response.body = body;
|
|
817
797
|
try {
|
|
818
798
|
response.text = body.toString('utf-8');
|
|
819
|
-
|
|
820
|
-
const ct = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : typeof contentTypeHeader === 'string' ? contentTypeHeader : undefined;
|
|
821
|
-
if (ct && ct.includes('application/json')) {
|
|
799
|
+
if (response.headers['content-type']?.includes('application/json')) {
|
|
822
800
|
response.json = JSON.parse(response.text);
|
|
823
801
|
}
|
|
824
802
|
}
|
|
825
803
|
catch { }
|
|
826
804
|
response.timing.end = Date.now() - timingStart;
|
|
827
805
|
response.timing.total = response.timing.end;
|
|
828
|
-
out.
|
|
806
|
+
if (!out.writableEnded && !out.destroyed)
|
|
807
|
+
out.end();
|
|
829
808
|
if (callback)
|
|
830
|
-
callback(null, response, response.body);
|
|
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);
|
|
831
819
|
});
|
|
832
|
-
stream.
|
|
833
|
-
out.
|
|
834
|
-
|
|
835
|
-
|
|
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();
|
|
836
828
|
});
|
|
829
|
+
out.on('drain', () => stream.resume());
|
|
830
|
+
stream.once('end', finish);
|
|
831
|
+
stream.once('error', finish);
|
|
837
832
|
if (post_data) {
|
|
838
|
-
if (Buffer.isBuffer(post_data) || typeof post_data === 'string')
|
|
839
|
-
stream.
|
|
840
|
-
|
|
841
|
-
|
|
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));
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
setImmediate(() => {
|
|
852
|
+
if (stream.writable)
|
|
853
|
+
stream.end();
|
|
854
|
+
});
|
|
855
|
+
}
|
|
842
856
|
}
|
|
843
857
|
else {
|
|
844
|
-
|
|
858
|
+
setImmediate(() => {
|
|
859
|
+
if (stream.writable)
|
|
860
|
+
stream.end();
|
|
861
|
+
});
|
|
845
862
|
}
|
|
846
863
|
}
|
|
847
864
|
async requestHttp1(path, options, timingStart, headers, post_data, out, callback) {
|
|
848
865
|
const reqOptions = {
|
|
849
866
|
method: options.method?.toUpperCase() || 'GET',
|
|
850
867
|
path,
|
|
851
|
-
headers: {
|
|
852
|
-
host: this.hostname,
|
|
853
|
-
...headers
|
|
854
|
-
},
|
|
868
|
+
headers: { host: this.hostname, ...headers },
|
|
855
869
|
createConnection: () => this.tlsSocket
|
|
856
870
|
};
|
|
857
|
-
const req = https.request(reqOptions);
|
|
871
|
+
const req = require('https').request(reqOptions);
|
|
858
872
|
const response = new events_1.EventEmitter();
|
|
859
873
|
response.timing = { socket: timingStart, lookup: 0, connect: 0, secureConnect: 0, response: 0, end: 0, total: 0 };
|
|
860
874
|
response.fingerprints = {
|
|
@@ -862,6 +876,54 @@ class UnifiedClientManager extends events_1.EventEmitter {
|
|
|
862
876
|
ja3Hash: crypto.createHash('md5').update(this.generateJA3()).digest('hex'),
|
|
863
877
|
akamai: ''
|
|
864
878
|
};
|
|
879
|
+
const chunks = [];
|
|
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;
|
|
894
|
+
}
|
|
895
|
+
let body = Buffer.concat(chunks);
|
|
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 { }
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
response.body = body;
|
|
913
|
+
try {
|
|
914
|
+
response.text = body.toString('utf-8');
|
|
915
|
+
if (res.headers['content-type']?.includes('application/json')) {
|
|
916
|
+
response.json = JSON.parse(response.text);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
catch { }
|
|
920
|
+
response.timing.end = Date.now() - timingStart;
|
|
921
|
+
response.timing.total = response.timing.end;
|
|
922
|
+
if (!out.writableEnded && !out.destroyed)
|
|
923
|
+
out.end();
|
|
924
|
+
if (callback)
|
|
925
|
+
setImmediate(() => callback(null, response, response.body));
|
|
926
|
+
};
|
|
865
927
|
req.once('response', (res) => {
|
|
866
928
|
response.statusCode = res.statusCode;
|
|
867
929
|
response.headers = res.headers;
|
|
@@ -870,68 +932,29 @@ class UnifiedClientManager extends events_1.EventEmitter {
|
|
|
870
932
|
response.timing.response = Date.now() - timingStart;
|
|
871
933
|
out.emit('header', response.statusCode, res.headers);
|
|
872
934
|
out.emit('headers', res.headers);
|
|
873
|
-
const chunks = [];
|
|
874
|
-
const rawChunks = [];
|
|
875
935
|
res.on('data', (chunk) => {
|
|
876
|
-
|
|
936
|
+
if (finished || out.destroyed || out.writableEnded)
|
|
937
|
+
return;
|
|
877
938
|
const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
878
939
|
rawChunks.push(bufferChunk);
|
|
879
940
|
chunks.push(bufferChunk);
|
|
880
941
|
out.write(bufferChunk);
|
|
881
942
|
});
|
|
882
|
-
res.once('end',
|
|
883
|
-
|
|
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);
|
|
943
|
+
res.once('end', () => finish(null, res));
|
|
944
|
+
res.once('error', finish);
|
|
929
945
|
});
|
|
946
|
+
req.once('error', finish);
|
|
930
947
|
if (post_data) {
|
|
931
|
-
if (Buffer.isBuffer(post_data) || typeof post_data === 'string')
|
|
932
|
-
req.
|
|
933
|
-
|
|
948
|
+
if (Buffer.isBuffer(post_data) || typeof post_data === 'string') {
|
|
949
|
+
req.write(post_data);
|
|
950
|
+
req.end();
|
|
951
|
+
}
|
|
952
|
+
else if (post_data instanceof stream_1.Readable) {
|
|
934
953
|
post_data.pipe(req);
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
req.end();
|
|
957
|
+
}
|
|
935
958
|
}
|
|
936
959
|
else {
|
|
937
960
|
req.end();
|
|
@@ -939,9 +962,9 @@ class UnifiedClientManager extends events_1.EventEmitter {
|
|
|
939
962
|
}
|
|
940
963
|
generateJA3() {
|
|
941
964
|
const version = '771';
|
|
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('-');
|
|
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('-');
|
|
945
968
|
const ecPoints = this.profile.tls.ecPointFormats.join('-');
|
|
946
969
|
return `${version},${ciphers},${extensions},${curves},${ecPoints}`;
|
|
947
970
|
}
|
|
@@ -950,9 +973,6 @@ class UnifiedClientManager extends events_1.EventEmitter {
|
|
|
950
973
|
this.http2Client.destroy();
|
|
951
974
|
this.http2Client = null;
|
|
952
975
|
}
|
|
953
|
-
getProtocol() {
|
|
954
|
-
return this.negotiatedProtocol;
|
|
955
|
-
}
|
|
956
976
|
}
|
|
957
977
|
class TLSSocketManager extends events_1.EventEmitter {
|
|
958
978
|
constructor(profile, proxy) {
|
|
@@ -983,6 +1003,7 @@ class TLSSocketManager extends events_1.EventEmitter {
|
|
|
983
1003
|
if (this.proxy) {
|
|
984
1004
|
const proxyUrl = new url.URL(this.proxy);
|
|
985
1005
|
this.socket = net.connect(+proxyUrl.port || 80, proxyUrl.hostname, () => {
|
|
1006
|
+
this.timing.connect = Date.now() - this.startTime;
|
|
986
1007
|
let request = `CONNECT ${hostname}:${port} HTTP/1.1\r\nHost: ${hostname}:${port}\r\n`;
|
|
987
1008
|
if (proxyUrl.username && proxyUrl.password) {
|
|
988
1009
|
const auth = Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString('base64');
|
|
@@ -992,14 +1013,19 @@ class TLSSocketManager extends events_1.EventEmitter {
|
|
|
992
1013
|
this.socket.write(request);
|
|
993
1014
|
});
|
|
994
1015
|
let response = '';
|
|
995
|
-
|
|
1016
|
+
const onData = (chunk) => {
|
|
996
1017
|
response += chunk.toString();
|
|
997
1018
|
if (response.includes('\r\n\r\n')) {
|
|
998
|
-
|
|
1019
|
+
this.socket.removeListener('data', onData);
|
|
1020
|
+
if (!response.startsWith('HTTP/1.1 200')) {
|
|
1021
|
+
clearTimeout(timeout);
|
|
1022
|
+
this.destroy();
|
|
999
1023
|
return reject(new Error('Proxy failed: ' + response.split('\r\n')[0]));
|
|
1024
|
+
}
|
|
1000
1025
|
this.proceedWithTLS(resolve, reject, timeout);
|
|
1001
1026
|
}
|
|
1002
|
-
}
|
|
1027
|
+
};
|
|
1028
|
+
this.socket.on('data', onData);
|
|
1003
1029
|
}
|
|
1004
1030
|
else {
|
|
1005
1031
|
this.socket = new net.Socket();
|
|
@@ -1176,7 +1202,7 @@ class AdvancedTLSClient {
|
|
|
1176
1202
|
if (options.multipart) {
|
|
1177
1203
|
const boundary = options.boundary || this.defaults.boundary;
|
|
1178
1204
|
post_data = await this.buildMultipart(data, boundary);
|
|
1179
|
-
config.headers['content-type'] =
|
|
1205
|
+
config.headers['content-type'] = `multipart/form-data; boundary=${boundary}`;
|
|
1180
1206
|
}
|
|
1181
1207
|
else if (data instanceof stream_1.Readable) {
|
|
1182
1208
|
post_data = data;
|
|
@@ -1202,78 +1228,63 @@ class AdvancedTLSClient {
|
|
|
1202
1228
|
const cookies = this.cookieJar.getCookies(hostname, parsed.pathname, parsed.protocol === 'https:');
|
|
1203
1229
|
if (cookies)
|
|
1204
1230
|
config.headers['cookie'] = cookies;
|
|
1205
|
-
|
|
1231
|
+
if (options.stream === true) {
|
|
1232
|
+
session.clientManager.request(path, options, startTime, config.headers, post_data, out);
|
|
1233
|
+
return out;
|
|
1234
|
+
}
|
|
1206
1235
|
if (callback) {
|
|
1207
|
-
|
|
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
|
+
});
|
|
1208
1241
|
return out;
|
|
1209
1242
|
}
|
|
1210
|
-
// MODE BARU: Kembalikan object response lengkap
|
|
1211
1243
|
return new Promise((resolve, reject) => {
|
|
1212
1244
|
let statusCode = 0;
|
|
1213
1245
|
let headers = {};
|
|
1214
1246
|
const chunks = [];
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
out.on('data', (chunk) => {
|
|
1223
|
-
chunks.push(chunk);
|
|
1224
|
-
});
|
|
1225
|
-
out.once('end', async () => {
|
|
1247
|
+
let done = false;
|
|
1248
|
+
const complete = async (err) => {
|
|
1249
|
+
if (done)
|
|
1250
|
+
return;
|
|
1251
|
+
done = true;
|
|
1252
|
+
if (err)
|
|
1253
|
+
return reject(err);
|
|
1226
1254
|
let body = Buffer.concat(chunks);
|
|
1227
|
-
// Auto-decompress jika diminta
|
|
1228
1255
|
if (options.decompress !== false && headers) {
|
|
1229
|
-
const
|
|
1230
|
-
const encoding = Array.isArray(encodingHeader)
|
|
1231
|
-
? encodingHeader[0]?.toLowerCase()
|
|
1232
|
-
: typeof encodingHeader === 'string'
|
|
1233
|
-
? encodingHeader.toLowerCase()
|
|
1234
|
-
: undefined;
|
|
1256
|
+
const encoding = headers['content-encoding']?.toLowerCase();
|
|
1235
1257
|
if (encoding) {
|
|
1236
1258
|
try {
|
|
1237
1259
|
if (encoding.includes('gzip'))
|
|
1238
|
-
body = await
|
|
1260
|
+
body = await gunzipAsync(body);
|
|
1239
1261
|
else if (encoding.includes('br'))
|
|
1240
|
-
body = await
|
|
1262
|
+
body = await brotliDecompressAsync(body);
|
|
1241
1263
|
else if (encoding.includes('deflate'))
|
|
1242
|
-
body = await
|
|
1243
|
-
}
|
|
1244
|
-
catch (e) {
|
|
1245
|
-
console.error('Decompression failed:', e);
|
|
1264
|
+
body = await inflateAsync(body);
|
|
1246
1265
|
}
|
|
1266
|
+
catch { }
|
|
1247
1267
|
}
|
|
1248
1268
|
}
|
|
1249
1269
|
const text = body.toString('utf-8');
|
|
1250
|
-
let parsedJson
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
? contentType[0]
|
|
1254
|
-
: typeof contentType === 'string'
|
|
1255
|
-
? contentType
|
|
1256
|
-
: undefined;
|
|
1257
|
-
if (ct && ct.includes('application/json')) {
|
|
1270
|
+
let parsedJson;
|
|
1271
|
+
const ct = headers['content-type']?.toLowerCase();
|
|
1272
|
+
if (ct?.includes('application/json')) {
|
|
1258
1273
|
try {
|
|
1259
1274
|
parsedJson = JSON.parse(text);
|
|
1260
1275
|
}
|
|
1261
1276
|
catch { }
|
|
1262
1277
|
}
|
|
1263
|
-
|
|
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 = {
|
|
1278
|
+
resolve({
|
|
1268
1279
|
statusCode,
|
|
1269
1280
|
headers,
|
|
1270
1281
|
body,
|
|
1271
1282
|
text,
|
|
1272
1283
|
json: parsedJson,
|
|
1273
1284
|
fingerprints: {
|
|
1274
|
-
ja3,
|
|
1275
|
-
ja3Hash,
|
|
1276
|
-
akamai
|
|
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`
|
|
1277
1288
|
},
|
|
1278
1289
|
timing: {
|
|
1279
1290
|
socket: startTime,
|
|
@@ -1284,19 +1295,26 @@ class AdvancedTLSClient {
|
|
|
1284
1295
|
end: Date.now() - startTime,
|
|
1285
1296
|
total: Date.now() - startTime
|
|
1286
1297
|
}
|
|
1287
|
-
};
|
|
1288
|
-
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
1300
|
+
out.on('header', (status, hdrs) => {
|
|
1301
|
+
statusCode = status;
|
|
1302
|
+
headers = hdrs;
|
|
1289
1303
|
});
|
|
1290
|
-
out.
|
|
1291
|
-
|
|
1292
|
-
|
|
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);
|
|
1293
1311
|
});
|
|
1294
1312
|
}
|
|
1295
1313
|
generateJA3() {
|
|
1296
1314
|
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('-');
|
|
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('-');
|
|
1300
1318
|
const ecPoints = this.profile.tls.ecPointFormats.join('-');
|
|
1301
1319
|
return `${version},${ciphers},${extensions},${curves},${ecPoints}`;
|
|
1302
1320
|
}
|
|
@@ -1318,30 +1336,18 @@ class AdvancedTLSClient {
|
|
|
1318
1336
|
done();
|
|
1319
1337
|
continue;
|
|
1320
1338
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
}
|
|
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(); });
|
|
1335
1341
|
}
|
|
1336
1342
|
});
|
|
1337
1343
|
}
|
|
1338
1344
|
async generateMultipart(name, part, boundary) {
|
|
1339
|
-
let return_part =
|
|
1340
|
-
return_part +=
|
|
1345
|
+
let return_part = `--${boundary}\r\n`;
|
|
1346
|
+
return_part += `Content-Disposition: form-data; name="${name}"`;
|
|
1341
1347
|
const append = (data, filename) => {
|
|
1342
1348
|
if (data) {
|
|
1343
|
-
return_part +=
|
|
1344
|
-
return_part +=
|
|
1349
|
+
return_part += `; filename="${encodeURIComponent(filename)}"\r\n`;
|
|
1350
|
+
return_part += `Content-Type: ${part.content_type || 'application/octet-stream'}\r\n\r\n`;
|
|
1345
1351
|
return_part += data.toString('binary');
|
|
1346
1352
|
}
|
|
1347
1353
|
return return_part + '\r\n';
|
|
@@ -1354,19 +1360,20 @@ class AdvancedTLSClient {
|
|
|
1354
1360
|
return append(data, filename);
|
|
1355
1361
|
}
|
|
1356
1362
|
else {
|
|
1357
|
-
return_part += '\r\n\r\n';
|
|
1358
|
-
return_part
|
|
1359
|
-
return return_part + '\r\n';
|
|
1363
|
+
return_part += '\r\n\r\n' + String(part.value || '') + '\r\n';
|
|
1364
|
+
return return_part;
|
|
1360
1365
|
}
|
|
1361
1366
|
}
|
|
1362
1367
|
flatten(object, into = {}, prefix) {
|
|
1363
1368
|
for (const key in object) {
|
|
1364
|
-
const prefix_key = prefix ? prefix
|
|
1369
|
+
const prefix_key = prefix ? `${prefix}[${key}]` : key;
|
|
1365
1370
|
const prop = object[key];
|
|
1366
|
-
if (prop && typeof prop === 'object' && !(prop.buffer || prop.file || prop.content_type))
|
|
1371
|
+
if (prop && typeof prop === 'object' && !(prop.buffer || prop.file || prop.content_type)) {
|
|
1367
1372
|
this.flatten(prop, into, prefix_key);
|
|
1368
|
-
|
|
1373
|
+
}
|
|
1374
|
+
else {
|
|
1369
1375
|
into[prefix_key] = prop;
|
|
1376
|
+
}
|
|
1370
1377
|
}
|
|
1371
1378
|
return into;
|
|
1372
1379
|
}
|