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.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
- for (const header of setCookieHeaders) {
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
- const [name, value] = nameValue.split('=');
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 [key, val] = parts[i].split('=');
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
- class TLSSocketManager extends events_1.EventEmitter {
667
- constructor(profile) {
668
- super();
669
- this.socket = null;
670
- this.tlsSocket = null;
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 {
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.client = null;
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 createSession() {
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.client = http2.connect(`https://${this.hostname}`, {
731
+ this.http2Client = http2.connect(`https://${this.hostname}`, {
809
732
  createConnection: () => this.tlsSocket,
810
733
  settings
811
734
  });
812
- this.client.once('connect', () => resolve(this.client));
813
- this.client.once('error', reject);
814
- this.client.once('timeout', () => reject(new Error('HTTP/2 session timeout')));
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 = {}, timingStart) {
818
- if (!this.client || this.client.destroyed) {
819
- throw new Error('HTTP/2 session not established or destroyed');
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
- const headers = {
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 defaultHeaders = {
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
- const chunks = [];
882
- stream.once('response', (headers, flags) => {
883
- response.statusCode = headers[':status'];
884
- response.headers = headers;
885
- const setCookies = headers['set-cookie'];
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
- if (options.decompress !== false) {
896
- const encoding = response.headers['content-encoding']?.toLowerCase();
897
- try {
898
- if (encoding === 'gzip')
899
- body = await gunzip(body);
900
- else if (encoding === 'br')
901
- body = await brotliDecompress(body);
902
- else if (encoding === 'deflate')
903
- body = await inflate(body);
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
- if ((response.headers['content-type'] || '').includes('application/json')) {
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
- response.emit('complete', response);
828
+ out.end();
829
+ if (callback)
830
+ callback(null, response, response.body);
918
831
  });
919
- stream.once('error', (err) => response.emit('error', err));
920
- stream.once('close', () => response.emit('close'));
921
- if (options.body) {
922
- if (Buffer.isBuffer(options.body) || typeof options.body === 'string') {
923
- stream.end(options.body);
924
- }
925
- else if (options.body instanceof stream_1.Readable) {
926
- options.body.pipe(stream);
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
- return new Promise((resolve, reject) => {
933
- response.once('complete', () => resolve(response));
934
- response.once('error', reject);
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
- .filter(c => c < 0xff00)
941
- .join('-');
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.client && !this.client.destroyed) {
953
- this.client.destroy();
954
- }
955
- this.client = null;
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.http2Manager.destroy();
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
- async request(url, options = {}) {
977
- const parsed = new URL(url);
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 http2Manager = new HTTP2ClientManager(tlsSocket, this.profile, hostname, this.cookieJar);
988
- await http2Manager.createSession();
989
- session = { tlsManager, http2Manager, lastUsed: Date.now() };
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].http2Manager.destroy();
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
- options.headers = options.headers || {};
1000
- options.headers['user-agent'] = this.profile.userAgent;
1001
- options.headers['accept-language'] = this.profile.acceptLanguage || 'en-US,en;q=0.9';
1002
- options.headers['accept-encoding'] = 'gzip, deflate, br';
1003
- options.headers['accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8';
1004
- options.headers['sec-ch-ua'] = this.profile.secChUa;
1005
- options.headers['sec-ch-ua-mobile'] = this.profile.secChUaMobile;
1006
- options.headers['sec-ch-ua-platform'] = this.profile.secChUaPlatform;
1007
- options.headers['sec-fetch-site'] = this.profile.secFetchSite;
1008
- options.headers['sec-fetch-mode'] = this.profile.secFetchMode;
1009
- options.headers['sec-fetch-dest'] = this.profile.secFetchDest;
1010
- if (this.profile.upgradeInsecureRequests)
1011
- options.headers['upgrade-insecure-requests'] = this.profile.upgradeInsecureRequests;
1012
- if (this.profile.priority)
1013
- options.headers['priority'] = this.profile.priority;
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
- options.headers['cookie'] = cookies;
1017
- let response = await session.http2Manager.request(path, options, startTime);
1018
- if (options.followRedirects !== false && [301, 302, 303, 307, 308].includes(response.statusCode) && (options.maxRedirects ?? 5) > 0) {
1019
- options.maxRedirects = (options.maxRedirects ?? 5) - 1;
1020
- const location = response.headers['location'];
1021
- if (location) {
1022
- const redirectUrl = new URL(location, url).toString();
1023
- return this.request(redirectUrl, options);
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 response;
1371
+ return into;
1027
1372
  }
1028
1373
  destroy() {
1029
1374
  clearInterval(this.cleanupInterval);
1030
1375
  this.sessionCache.forEach(s => {
1031
- s.http2Manager.destroy();
1376
+ s.clientManager.destroy();
1032
1377
  s.tlsManager.destroy();
1033
1378
  });
1034
1379
  this.sessionCache.clear();