advanced-tls-client 3.0.1 → 3.0.3

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 CHANGED
@@ -1,3 +1,4 @@
1
+ import { EventEmitter } from 'events';
1
2
  import { Readable, PassThrough } from 'stream';
2
3
  interface TLSFingerprint {
3
4
  cipherSuites: number[];
@@ -119,13 +120,36 @@ interface RequestOptions {
119
120
  content_type?: string;
120
121
  uri_modifier?: Function;
121
122
  }
123
+ interface Response extends EventEmitter {
124
+ statusCode: number;
125
+ headers: Record<string, string | string[]>;
126
+ body: Buffer | Readable | any;
127
+ text?: string;
128
+ json?: any;
129
+ timing: {
130
+ socket: number;
131
+ lookup: number;
132
+ connect: number;
133
+ secureConnect: number;
134
+ response: number;
135
+ end: number;
136
+ total: number;
137
+ };
138
+ fingerprints: {
139
+ ja3: string;
140
+ ja3Hash: string;
141
+ akamai: string;
142
+ };
143
+ bytes?: number;
144
+ raw?: Buffer;
145
+ cookies?: Record<string, string>;
146
+ parser?: string;
147
+ }
122
148
  declare class ProfileRegistry {
123
149
  private static getGrease;
124
150
  static chromeMobile143Android(): ChromeProfile;
125
151
  static chrome133PSK(): ChromeProfile;
126
152
  static chrome133(): ChromeProfile;
127
- static firefox117(): ChromeProfile;
128
- static safariIOS18(): ChromeProfile;
129
153
  static get(name: string): ChromeProfile;
130
154
  static get latest(): ChromeProfile;
131
155
  static get latestMobile(): ChromeProfile;
@@ -139,12 +163,12 @@ declare class AdvancedTLSClient {
139
163
  private cleanupInterval;
140
164
  private defaults;
141
165
  constructor(profileName?: string);
142
- setup(uri: string, options: RequestOptions): any;
143
- request(uri: string, data?: any, options?: RequestOptions, callback?: Function): Promise<FullResponse | PassThrough>;
166
+ private setup;
167
+ request(uri: string, data?: any, options?: RequestOptions, callback?: (err: any, res?: Response) => void): Promise<FullResponse | PassThrough>;
144
168
  private generateJA3;
145
169
  private buildMultipart;
146
170
  private generateMultipart;
147
171
  private flatten;
148
172
  destroy(): void;
149
173
  }
150
- export { AdvancedTLSClient, ProfileRegistry, FullResponse };
174
+ export { AdvancedTLSClient, ProfileRegistry };
package/dist/index.js CHANGED
@@ -46,9 +46,9 @@ const stream_1 = require("stream");
46
46
  const fs = __importStar(require("fs"));
47
47
  const path = __importStar(require("path"));
48
48
  const url = __importStar(require("url"));
49
- const gunzip = (0, util_1.promisify)(zlib.gunzip);
50
- const brotliDecompress = (0, util_1.promisify)(zlib.brotliDecompress);
51
- const inflate = (0, util_1.promisify)(zlib.inflate);
49
+ const brotliDecompressAsync = (0, util_1.promisify)(zlib.brotliDecompress);
50
+ const gunzipAsync = (0, util_1.promisify)(zlib.gunzip);
51
+ const inflateAsync = (0, util_1.promisify)(zlib.inflate);
52
52
  const GREASE_VALUES = [0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a, 0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa];
53
53
  class ProfileRegistry {
54
54
  static getGrease() {
@@ -414,202 +414,16 @@ class ProfileRegistry {
414
414
  secFetchDest: 'document'
415
415
  };
416
416
  }
417
- static firefox117() {
418
- return {
419
- name: 'firefox_117',
420
- version: '117.0',
421
- tls: {
422
- cipherSuites: [
423
- 4865,
424
- 4867,
425
- 4866,
426
- 49195,
427
- 49199,
428
- 52393,
429
- 52392,
430
- 49196,
431
- 49200,
432
- 159,
433
- 158,
434
- 49161,
435
- 49162,
436
- 156,
437
- 157,
438
- 47,
439
- 53
440
- ],
441
- extensions: [0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21],
442
- supportedGroups: [
443
- 29,
444
- 23,
445
- 24,
446
- 25,
447
- 256,
448
- 257
449
- ],
450
- signatureAlgorithms: [
451
- 1027,
452
- 1283,
453
- 1539,
454
- 2052,
455
- 2053,
456
- 2054,
457
- 1025,
458
- 1281,
459
- 1537,
460
- 513,
461
- 515
462
- ],
463
- supportedVersions: [
464
- 772,
465
- 771
466
- ],
467
- ecPointFormats: [0],
468
- alpnProtocols: ['h2', 'http/1.1'],
469
- pskKeyExchangeModes: [1],
470
- compressionMethods: [0],
471
- grease: false
472
- },
473
- http2: {
474
- windowUpdate: 12517377,
475
- headerTableSize: 65536,
476
- enablePush: 0,
477
- initialWindowSize: 131072,
478
- maxFrameSize: 16384,
479
- settingsOrder: [1, 4, 5],
480
- connectionPreface: Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'),
481
- priorityFrames: [
482
- { streamId: 3, weight: 200, dependency: 0, exclusive: false },
483
- { streamId: 5, weight: 100, dependency: 0, exclusive: false },
484
- { streamId: 7, weight: 0, dependency: 0, exclusive: false },
485
- { streamId: 9, weight: 0, dependency: 7, exclusive: false },
486
- { streamId: 11, weight: 0, dependency: 3, exclusive: false },
487
- { streamId: 13, weight: 240, dependency: 0, exclusive: false }
488
- ],
489
- pseudoHeaderOrder: [':method', ':path', ':authority', ':scheme'],
490
- headerPriority: { streamDep: 13, exclusive: false, weight: 41 },
491
- headerOrder: []
492
- },
493
- userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:117.0) Gecko/20100101 Firefox/117.0',
494
- secChUa: '',
495
- secChUaPlatform: '',
496
- secChUaMobile: '',
497
- secFetchSite: 'none',
498
- secFetchMode: 'navigate',
499
- secFetchDest: 'document'
500
- };
501
- }
502
- static safariIOS18() {
503
- const grease1 = this.getGrease();
504
- const grease2 = this.getGrease();
505
- return {
506
- name: 'safari_ios_18_0',
507
- version: '18.0',
508
- tls: {
509
- cipherSuites: [
510
- grease1,
511
- 4865,
512
- 4866,
513
- 4867,
514
- 49196,
515
- 49195,
516
- 52393,
517
- 49200,
518
- 49199,
519
- 52392,
520
- 159,
521
- 158,
522
- 49162,
523
- 49161,
524
- 157,
525
- 156,
526
- 53,
527
- 47,
528
- 49188,
529
- 49187,
530
- 60
531
- ],
532
- extensions: [
533
- grease2,
534
- 0,
535
- 23,
536
- 65281,
537
- 10,
538
- 11,
539
- 16,
540
- 5,
541
- 13,
542
- 18,
543
- 51,
544
- 45,
545
- 43,
546
- 21
547
- ],
548
- supportedGroups: [
549
- 29,
550
- 23,
551
- 24,
552
- 25
553
- ],
554
- signatureAlgorithms: [
555
- 1027,
556
- 2052,
557
- 1025,
558
- 1283,
559
- 513,
560
- 2053,
561
- 2053,
562
- 1281,
563
- 2054,
564
- 1537,
565
- 515
566
- ],
567
- supportedVersions: [
568
- 772,
569
- 771,
570
- 770,
571
- 769
572
- ],
573
- ecPointFormats: [0],
574
- alpnProtocols: ['h2', 'http/1.1'],
575
- pskKeyExchangeModes: [1],
576
- compressionMethods: [0],
577
- grease: true
578
- },
579
- http2: {
580
- windowUpdate: 10420225,
581
- headerTableSize: 4096,
582
- enablePush: 0,
583
- maxConcurrentStreams: 100,
584
- initialWindowSize: 2097152,
585
- maxFrameSize: 16384,
586
- settingsOrder: [2, 3, 4, 8, 9],
587
- connectionPreface: Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'),
588
- priorityFrames: [],
589
- pseudoHeaderOrder: [':method', ':scheme', ':authority', ':path'],
590
- headerOrder: []
591
- },
592
- userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1',
593
- secChUa: '',
594
- secChUaPlatform: '',
595
- secChUaMobile: '?1',
596
- secFetchSite: 'none',
597
- secFetchMode: 'navigate',
598
- secFetchDest: 'document'
599
- };
600
- }
601
417
  static get(name) {
602
418
  switch (name.toLowerCase()) {
603
419
  case 'chrome_mobile_143_android': return this.chromeMobile143Android();
604
420
  case 'chrome_133_psk': return this.chrome133PSK();
605
421
  case 'chrome_133': return this.chrome133();
606
- case 'firefox_117': return this.firefox117();
607
- case 'safari_ios_18_0': return this.safariIOS18();
608
- default: return this.chrome133();
422
+ default: return this.chromeMobile143Android();
609
423
  }
610
424
  }
611
425
  static get latest() {
612
- return this.chrome133();
426
+ return this.chromeMobile143Android();
613
427
  }
614
428
  static get latestMobile() {
615
429
  return this.chromeMobile143Android();
@@ -675,16 +489,15 @@ class CookieJar {
675
489
  }
676
490
  }
677
491
  }
678
- var Protocol;
679
- (function (Protocol) {
680
- Protocol["HTTP1"] = "http1";
681
- Protocol["HTTP2"] = "http2";
682
- })(Protocol || (Protocol = {}));
492
+ const Protocol = {
493
+ HTTP1: 'http1',
494
+ HTTP2: 'http2'
495
+ };
683
496
  class UnifiedClientManager extends events_1.EventEmitter {
684
497
  constructor(tlsSocket, profile, hostname, cookieJar) {
685
498
  super();
686
499
  this.http2Client = null;
687
- this.negotiatedProtocol = Protocol.HTTP1;
500
+ this.negotiatedProtocol = 'http1';
688
501
  this.tlsSocket = tlsSocket;
689
502
  this.profile = profile;
690
503
  this.hostname = hostname;
@@ -693,17 +506,17 @@ class UnifiedClientManager extends events_1.EventEmitter {
693
506
  async initialize() {
694
507
  const alpn = this.tlsSocket.alpnProtocol;
695
508
  if (alpn === 'h2') {
696
- this.negotiatedProtocol = Protocol.HTTP2;
509
+ this.negotiatedProtocol = 'http2';
697
510
  await this.createHttp2Session();
698
511
  }
699
512
  else {
700
- this.negotiatedProtocol = Protocol.HTTP1;
513
+ this.negotiatedProtocol = 'http1';
701
514
  }
702
515
  }
703
516
  async createHttp2Session() {
704
517
  return new Promise((resolve, reject) => {
705
518
  const settings = {};
706
- this.profile.http2.settingsOrder.forEach(id => {
519
+ this.profile.http2.settingsOrder.forEach((id) => {
707
520
  switch (id) {
708
521
  case 1:
709
522
  settings.headerTableSize = this.profile.http2.headerTableSize;
@@ -711,17 +524,9 @@ class UnifiedClientManager extends events_1.EventEmitter {
711
524
  case 2:
712
525
  settings.enablePush = this.profile.http2.enablePush === 1;
713
526
  break;
714
- case 3:
715
- if (this.profile.http2.maxConcurrentStreams)
716
- settings.maxConcurrentStreams = this.profile.http2.maxConcurrentStreams;
717
- break;
718
527
  case 4:
719
528
  settings.initialWindowSize = this.profile.http2.initialWindowSize;
720
529
  break;
721
- case 5:
722
- if (this.profile.http2.maxFrameSize)
723
- settings.maxFrameSize = this.profile.http2.maxFrameSize;
724
- break;
725
530
  case 6:
726
531
  if (this.profile.http2.maxHeaderListSize)
727
532
  settings.maxHeaderListSize = this.profile.http2.maxHeaderListSize;
@@ -732,7 +537,7 @@ class UnifiedClientManager extends events_1.EventEmitter {
732
537
  createConnection: () => this.tlsSocket,
733
538
  settings
734
539
  });
735
- this.http2Client.once('connect', () => resolve());
540
+ this.http2Client.once('connect', resolve);
736
541
  this.http2Client.once('error', reject);
737
542
  this.http2Client.once('timeout', () => reject(new Error('HTTP/2 session timeout')));
738
543
  });
@@ -749,7 +554,7 @@ class UnifiedClientManager extends events_1.EventEmitter {
749
554
  delete cleanHeaders['Upgrade'];
750
555
  delete cleanHeaders['transfer-encoding'];
751
556
  delete cleanHeaders['Transfer-Encoding'];
752
- if (this.negotiatedProtocol === Protocol.HTTP2 && this.http2Client && !this.http2Client.destroyed) {
557
+ if (this.negotiatedProtocol === 'http2' && this.http2Client && !this.http2Client.destroyed) {
753
558
  await this.requestHttp2(path, options, timingStart, cleanHeaders, post_data, out, callback);
754
559
  }
755
560
  else {
@@ -757,13 +562,12 @@ class UnifiedClientManager extends events_1.EventEmitter {
757
562
  }
758
563
  }
759
564
  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');
565
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
762
566
  const reqHeaders = {
763
567
  ':method': options.method?.toUpperCase() || 'GET',
764
568
  ':authority': this.hostname,
765
569
  ':scheme': 'https',
766
- ':path': path,
570
+ ':path': normalizedPath,
767
571
  ...headers
768
572
  };
769
573
  const stream = this.http2Client.request(reqHeaders);
@@ -774,84 +578,112 @@ class UnifiedClientManager extends events_1.EventEmitter {
774
578
  ja3Hash: crypto.createHash('md5').update(this.generateJA3()).digest('hex'),
775
579
  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
580
  };
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
581
  const chunks = [];
787
582
  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);
794
- });
795
- stream.once('end', async () => {
583
+ let finished = false;
584
+ let hasHeaders = false;
585
+ const finish = async (error) => {
586
+ if (finished)
587
+ return;
588
+ finished = true;
589
+ stream.removeAllListeners();
590
+ if (error) {
591
+ out.emit('error', error);
592
+ if (callback)
593
+ setImmediate(() => callback(error));
594
+ return;
595
+ }
796
596
  let body = Buffer.concat(chunks);
797
597
  response.bytes = body.length;
798
598
  response.raw = Buffer.concat(rawChunks);
799
599
  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;
600
+ const encoding = response.headers['content-encoding']?.toLowerCase();
802
601
  if (encoding) {
803
602
  try {
804
603
  if (encoding.includes('gzip'))
805
- body = await gunzip(body);
604
+ body = await gunzipAsync(body);
806
605
  else if (encoding.includes('br'))
807
- body = await brotliDecompress(body);
606
+ body = await brotliDecompressAsync(body);
808
607
  else if (encoding.includes('deflate'))
809
- body = await inflate(body);
810
- }
811
- catch (e) {
812
- console.error('Decompression error:', e);
608
+ body = await inflateAsync(body);
813
609
  }
610
+ catch { }
814
611
  }
815
612
  }
816
613
  response.body = body;
817
614
  try {
818
615
  response.text = body.toString('utf-8');
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')) {
616
+ if (response.headers['content-type']?.includes('application/json')) {
822
617
  response.json = JSON.parse(response.text);
823
618
  }
824
619
  }
825
620
  catch { }
826
621
  response.timing.end = Date.now() - timingStart;
827
622
  response.timing.total = response.timing.end;
828
- out.end();
623
+ if (!out.writableEnded && !out.destroyed)
624
+ out.end();
625
+ };
626
+ stream.once('response', (hdrs) => {
627
+ hasHeaders = true;
628
+ response.statusCode = hdrs[':status'];
629
+ response.headers = hdrs;
630
+ if (hdrs['set-cookie'])
631
+ this.cookieJar.parseSetCookie(this.hostname, hdrs['set-cookie']);
632
+ response.timing.response = Date.now() - timingStart;
633
+ out.emit('header', response.statusCode, hdrs);
634
+ out.emit('headers', hdrs);
829
635
  if (callback)
830
- callback(null, response, response.body);
636
+ setImmediate(() => callback(null, response));
831
637
  });
832
- stream.once('error', (err) => {
833
- out.emit('err', err);
834
- if (callback)
835
- callback(err);
638
+ stream.on('data', (chunk) => {
639
+ if (finished || out.destroyed || out.writableEnded)
640
+ return;
641
+ const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
642
+ rawChunks.push(bufferChunk);
643
+ chunks.push(bufferChunk);
644
+ if (!out.write(bufferChunk))
645
+ stream.pause();
836
646
  });
647
+ out.on('drain', () => stream.resume());
648
+ stream.once('end', finish);
649
+ stream.once('error', finish);
837
650
  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);
651
+ if (Buffer.isBuffer(post_data) || typeof post_data === 'string') {
652
+ if (!stream.closed && !stream.destroyed && stream.writable) {
653
+ try {
654
+ stream.write(post_data);
655
+ }
656
+ catch { }
657
+ }
658
+ setImmediate(() => {
659
+ if (!stream.closed && !stream.destroyed && stream.writable) {
660
+ stream.end();
661
+ }
662
+ });
663
+ }
664
+ else if (post_data instanceof stream_1.Readable) {
665
+ post_data.pipe(stream, { end: true });
666
+ post_data.on('error', err => stream.destroy(err));
667
+ }
668
+ else {
669
+ setImmediate(() => {
670
+ if (stream.writable)
671
+ stream.end();
672
+ });
673
+ }
842
674
  }
843
675
  else {
844
- stream.end();
676
+ setImmediate(() => {
677
+ if (stream.writable)
678
+ stream.end();
679
+ });
845
680
  }
846
681
  }
847
682
  async requestHttp1(path, options, timingStart, headers, post_data, out, callback) {
848
683
  const reqOptions = {
849
684
  method: options.method?.toUpperCase() || 'GET',
850
685
  path,
851
- headers: {
852
- host: this.hostname,
853
- ...headers
854
- },
686
+ headers: { host: this.hostname, ...headers },
855
687
  createConnection: () => this.tlsSocket
856
688
  };
857
689
  const req = https.request(reqOptions);
@@ -862,7 +694,55 @@ class UnifiedClientManager extends events_1.EventEmitter {
862
694
  ja3Hash: crypto.createHash('md5').update(this.generateJA3()).digest('hex'),
863
695
  akamai: ''
864
696
  };
697
+ const chunks = [];
698
+ const rawChunks = [];
699
+ let finished = false;
700
+ let hasHeaders = false;
701
+ const finish = async (error, res) => {
702
+ if (finished)
703
+ return;
704
+ finished = true;
705
+ req.removeAllListeners();
706
+ if (res)
707
+ res.removeAllListeners();
708
+ if (error) {
709
+ out.emit('error', error);
710
+ if (callback)
711
+ setImmediate(() => callback(error));
712
+ return;
713
+ }
714
+ let body = Buffer.concat(chunks);
715
+ response.bytes = body.length;
716
+ response.raw = Buffer.concat(rawChunks);
717
+ if (options.decompress !== false && res?.headers) {
718
+ const encoding = (res.headers['content-encoding'] || '').toLowerCase();
719
+ if (encoding) {
720
+ try {
721
+ if (encoding.includes('gzip'))
722
+ body = await gunzipAsync(body);
723
+ else if (encoding.includes('br'))
724
+ body = await brotliDecompressAsync(body);
725
+ else if (encoding.includes('deflate'))
726
+ body = await inflateAsync(body);
727
+ }
728
+ catch { }
729
+ }
730
+ }
731
+ response.body = body;
732
+ try {
733
+ response.text = body.toString('utf-8');
734
+ if (res.headers['content-type']?.includes('application/json')) {
735
+ response.json = JSON.parse(response.text);
736
+ }
737
+ }
738
+ catch { }
739
+ response.timing.end = Date.now() - timingStart;
740
+ response.timing.total = response.timing.end;
741
+ if (!out.writableEnded && !out.destroyed)
742
+ out.end();
743
+ };
865
744
  req.once('response', (res) => {
745
+ hasHeaders = true;
866
746
  response.statusCode = res.statusCode;
867
747
  response.headers = res.headers;
868
748
  if (res.headers['set-cookie'])
@@ -870,68 +750,31 @@ class UnifiedClientManager extends events_1.EventEmitter {
870
750
  response.timing.response = Date.now() - timingStart;
871
751
  out.emit('header', response.statusCode, res.headers);
872
752
  out.emit('headers', res.headers);
873
- const chunks = [];
874
- const rawChunks = [];
753
+ if (callback)
754
+ setImmediate(() => callback(null, response));
875
755
  res.on('data', (chunk) => {
876
- // Convert string to Buffer if needed
756
+ if (finished || out.destroyed || out.writableEnded)
757
+ return;
877
758
  const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
878
759
  rawChunks.push(bufferChunk);
879
760
  chunks.push(bufferChunk);
880
761
  out.write(bufferChunk);
881
762
  });
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);
763
+ res.once('end', () => finish(null, res));
764
+ res.once('error', finish);
929
765
  });
766
+ req.once('error', finish);
930
767
  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)
768
+ if (Buffer.isBuffer(post_data) || typeof post_data === 'string') {
769
+ req.write(post_data);
770
+ req.end();
771
+ }
772
+ else if (post_data instanceof stream_1.Readable) {
934
773
  post_data.pipe(req);
774
+ }
775
+ else {
776
+ req.end();
777
+ }
935
778
  }
936
779
  else {
937
780
  req.end();
@@ -939,9 +782,9 @@ class UnifiedClientManager extends events_1.EventEmitter {
939
782
  }
940
783
  generateJA3() {
941
784
  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('-');
785
+ const ciphers = this.profile.tls.cipherSuites.filter((c) => c < 0xff00).join('-');
786
+ const extensions = this.profile.tls.extensions.filter((e) => typeof e === 'number' && e < 0xff00).join('-');
787
+ const curves = this.profile.tls.supportedGroups.filter((g) => g < 0xff00).join('-');
945
788
  const ecPoints = this.profile.tls.ecPointFormats.join('-');
946
789
  return `${version},${ciphers},${extensions},${curves},${ecPoints}`;
947
790
  }
@@ -950,9 +793,6 @@ class UnifiedClientManager extends events_1.EventEmitter {
950
793
  this.http2Client.destroy();
951
794
  this.http2Client = null;
952
795
  }
953
- getProtocol() {
954
- return this.negotiatedProtocol;
955
- }
956
796
  }
957
797
  class TLSSocketManager extends events_1.EventEmitter {
958
798
  constructor(profile, proxy) {
@@ -983,6 +823,7 @@ class TLSSocketManager extends events_1.EventEmitter {
983
823
  if (this.proxy) {
984
824
  const proxyUrl = new url.URL(this.proxy);
985
825
  this.socket = net.connect(+proxyUrl.port || 80, proxyUrl.hostname, () => {
826
+ this.timing.connect = Date.now() - this.startTime;
986
827
  let request = `CONNECT ${hostname}:${port} HTTP/1.1\r\nHost: ${hostname}:${port}\r\n`;
987
828
  if (proxyUrl.username && proxyUrl.password) {
988
829
  const auth = Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString('base64');
@@ -992,14 +833,19 @@ class TLSSocketManager extends events_1.EventEmitter {
992
833
  this.socket.write(request);
993
834
  });
994
835
  let response = '';
995
- this.socket.on('data', (chunk) => {
836
+ const onData = (chunk) => {
996
837
  response += chunk.toString();
997
838
  if (response.includes('\r\n\r\n')) {
998
- if (!response.startsWith('HTTP/1.1 200'))
839
+ this.socket.removeListener('data', onData);
840
+ if (!response.startsWith('HTTP/1.1 200')) {
841
+ clearTimeout(timeout);
842
+ this.destroy();
999
843
  return reject(new Error('Proxy failed: ' + response.split('\r\n')[0]));
844
+ }
1000
845
  this.proceedWithTLS(resolve, reject, timeout);
1001
846
  }
1002
- });
847
+ };
848
+ this.socket.on('data', onData);
1003
849
  }
1004
850
  else {
1005
851
  this.socket = new net.Socket();
@@ -1107,7 +953,7 @@ class AdvancedTLSClient {
1107
953
  follow_if_same_location: false,
1108
954
  use_proxy_from_env_var: true
1109
955
  };
1110
- this.profile = ProfileRegistry.get(profileName || 'chrome_133');
956
+ this.profile = ProfileRegistry.get(profileName || 'chrome_mobile_143_android');
1111
957
  this.cleanupInterval = setInterval(() => {
1112
958
  const now = Date.now();
1113
959
  for (const [key, session] of this.sessionCache) {
@@ -1121,7 +967,7 @@ class AdvancedTLSClient {
1121
967
  }
1122
968
  setup(uri, options) {
1123
969
  const config = {
1124
- headers: { ...options.headers },
970
+ headers: {},
1125
971
  proxy: options.proxy || this.defaults.proxy,
1126
972
  decompress: options.decompress !== undefined ? options.decompress : true
1127
973
  };
@@ -1139,6 +985,10 @@ class AdvancedTLSClient {
1139
985
  config.headers['upgrade-insecure-requests'] = this.profile.upgradeInsecureRequests;
1140
986
  if (this.profile.priority)
1141
987
  config.headers['priority'] = this.profile.priority;
988
+ // Override with user headers
989
+ if (options.headers) {
990
+ Object.assign(config.headers, options.headers);
991
+ }
1142
992
  return config;
1143
993
  }
1144
994
  async request(uri, data, options = {}, callback) {
@@ -1146,6 +996,9 @@ class AdvancedTLSClient {
1146
996
  callback = options;
1147
997
  options = {};
1148
998
  }
999
+ if (data && typeof data !== 'object' && !Buffer.isBuffer(data) && !(data instanceof stream_1.Readable)) {
1000
+ throw new Error('Invalid data type');
1001
+ }
1149
1002
  const parsed = new url.URL(uri);
1150
1003
  const hostname = parsed.hostname;
1151
1004
  const port = parsed.port ? +parsed.port : 443;
@@ -1171,12 +1024,15 @@ class AdvancedTLSClient {
1171
1024
  session.lastUsed = Date.now();
1172
1025
  const config = this.setup(uri, options);
1173
1026
  let post_data = null;
1174
- let json = options.json || (options.json !== false && config.headers['content-type']?.includes('application/json'));
1027
+ let json = options.json !== undefined ? options.json : false;
1028
+ if (json === undefined && config.headers['content-type']?.includes('application/json')) {
1029
+ json = true;
1030
+ }
1175
1031
  if (data) {
1176
1032
  if (options.multipart) {
1177
1033
  const boundary = options.boundary || this.defaults.boundary;
1178
1034
  post_data = await this.buildMultipart(data, boundary);
1179
- config.headers['content-type'] = 'multipart/form-data; boundary=' + boundary;
1035
+ config.headers['content-type'] = `multipart/form-data; boundary=${boundary}`;
1180
1036
  }
1181
1037
  else if (data instanceof stream_1.Readable) {
1182
1038
  post_data = data;
@@ -1202,78 +1058,62 @@ class AdvancedTLSClient {
1202
1058
  const cookies = this.cookieJar.getCookies(hostname, parsed.pathname, parsed.protocol === 'https:');
1203
1059
  if (cookies)
1204
1060
  config.headers['cookie'] = cookies;
1205
- // MODE LEGACY: Jika ada callback, kembalikan stream
1061
+ if (options.stream === true) {
1062
+ session.clientManager.request(path, options, startTime, config.headers, post_data, out);
1063
+ return out;
1064
+ }
1206
1065
  if (callback) {
1207
- await session.clientManager.request(path, options, startTime, config.headers, post_data, out, callback);
1066
+ session.clientManager.request(path, options, startTime, config.headers, post_data, out, callback).catch(err => {
1067
+ if (callback)
1068
+ callback(err);
1069
+ });
1208
1070
  return out;
1209
1071
  }
1210
- // MODE BARU: Kembalikan object response lengkap
1211
1072
  return new Promise((resolve, reject) => {
1212
1073
  let statusCode = 0;
1213
1074
  let headers = {};
1214
1075
  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 () => {
1076
+ let done = false;
1077
+ const complete = async (err) => {
1078
+ if (done)
1079
+ return;
1080
+ done = true;
1081
+ if (err)
1082
+ return reject(err);
1226
1083
  let body = Buffer.concat(chunks);
1227
- // Auto-decompress jika diminta
1228
1084
  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;
1085
+ const encoding = headers['content-encoding']?.toLowerCase();
1235
1086
  if (encoding) {
1236
1087
  try {
1237
1088
  if (encoding.includes('gzip'))
1238
- body = await gunzip(body);
1089
+ body = await gunzipAsync(body);
1239
1090
  else if (encoding.includes('br'))
1240
- body = await brotliDecompress(body);
1091
+ body = await brotliDecompressAsync(body);
1241
1092
  else if (encoding.includes('deflate'))
1242
- body = await inflate(body);
1243
- }
1244
- catch (e) {
1245
- console.error('Decompression failed:', e);
1093
+ body = await inflateAsync(body);
1246
1094
  }
1095
+ catch { }
1247
1096
  }
1248
1097
  }
1249
1098
  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')) {
1099
+ let parsedJson;
1100
+ const ct = headers['content-type']?.toLowerCase();
1101
+ if (ct?.includes('application/json')) {
1258
1102
  try {
1259
1103
  parsedJson = JSON.parse(text);
1260
1104
  }
1261
1105
  catch { }
1262
1106
  }
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 = {
1107
+ resolve({
1268
1108
  statusCode,
1269
1109
  headers,
1270
1110
  body,
1271
1111
  text,
1272
1112
  json: parsedJson,
1273
1113
  fingerprints: {
1274
- ja3,
1275
- ja3Hash,
1276
- akamai
1114
+ ja3: this.generateJA3(),
1115
+ ja3Hash: crypto.createHash('md5').update(this.generateJA3()).digest('hex'),
1116
+ 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
1117
  },
1278
1118
  timing: {
1279
1119
  socket: startTime,
@@ -1284,19 +1124,26 @@ class AdvancedTLSClient {
1284
1124
  end: Date.now() - startTime,
1285
1125
  total: Date.now() - startTime
1286
1126
  }
1287
- };
1288
- resolve(fullResponse);
1127
+ });
1128
+ };
1129
+ out.on('header', (status, hdrs) => {
1130
+ statusCode = status;
1131
+ headers = hdrs;
1289
1132
  });
1290
- out.once('error', (err) => reject(err));
1291
- // Jalankan request
1292
- session.clientManager.request(path, options, startTime, config.headers, post_data, out).catch(reject);
1133
+ out.on('data', (chunk) => {
1134
+ if (!done)
1135
+ chunks.push(chunk);
1136
+ });
1137
+ out.once('end', () => complete());
1138
+ out.once('error', complete);
1139
+ session.clientManager.request(path, options, startTime, config.headers, post_data, out).catch(complete);
1293
1140
  });
1294
1141
  }
1295
1142
  generateJA3() {
1296
1143
  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('-');
1144
+ const ciphers = this.profile.tls.cipherSuites.filter((c) => c < 0xff00).join('-');
1145
+ const extensions = this.profile.tls.extensions.filter((e) => typeof e === 'number' && e < 0xff00).join('-');
1146
+ const curves = this.profile.tls.supportedGroups.filter((g) => g < 0xff00).join('-');
1300
1147
  const ecPoints = this.profile.tls.ecPointFormats.join('-');
1301
1148
  return `${version},${ciphers},${extensions},${curves},${ecPoints}`;
1302
1149
  }
@@ -1318,30 +1165,18 @@ class AdvancedTLSClient {
1318
1165
  done();
1319
1166
  continue;
1320
1167
  }
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
- }
1168
+ const part = Buffer.isBuffer(value) ? { buffer: value, content_type: 'application/octet-stream' } : (value.buffer || value.file || value.content_type) ? value : { value };
1169
+ this.generateMultipart(key, part, boundary).then(section => { body += section; done(); });
1335
1170
  }
1336
1171
  });
1337
1172
  }
1338
1173
  async generateMultipart(name, part, boundary) {
1339
- let return_part = '--' + boundary + '\r\n';
1340
- return_part += 'Content-Disposition: form-data; name="' + name + '"';
1174
+ let return_part = `--${boundary}\r\n`;
1175
+ return_part += `Content-Disposition: form-data; name="${name}"`;
1341
1176
  const append = (data, filename) => {
1342
1177
  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';
1178
+ return_part += `; filename="${encodeURIComponent(filename)}"\r\n`;
1179
+ return_part += `Content-Type: ${part.content_type || 'application/octet-stream'}\r\n\r\n`;
1345
1180
  return_part += data.toString('binary');
1346
1181
  }
1347
1182
  return return_part + '\r\n';
@@ -1354,19 +1189,20 @@ class AdvancedTLSClient {
1354
1189
  return append(data, filename);
1355
1190
  }
1356
1191
  else {
1357
- return_part += '\r\n\r\n';
1358
- return_part += String(part.value || '');
1359
- return return_part + '\r\n';
1192
+ return_part += '\r\n\r\n' + String(part.value || '') + '\r\n';
1193
+ return return_part;
1360
1194
  }
1361
1195
  }
1362
1196
  flatten(object, into = {}, prefix) {
1363
1197
  for (const key in object) {
1364
- const prefix_key = prefix ? prefix + '[' + key + ']' : key;
1198
+ const prefix_key = prefix ? `${prefix}[${key}]` : key;
1365
1199
  const prop = object[key];
1366
- if (prop && typeof prop === 'object' && !(prop.buffer || prop.file || prop.content_type))
1200
+ if (prop && typeof prop === 'object' && !(prop.buffer || prop.file || prop.content_type)) {
1367
1201
  this.flatten(prop, into, prefix_key);
1368
- else
1202
+ }
1203
+ else {
1369
1204
  into[prefix_key] = prop;
1205
+ }
1370
1206
  }
1371
1207
  return into;
1372
1208
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "advanced-tls-client",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {