advanced-tls-client 1.0.2 → 3.0.2

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