Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/Emulation/Browser.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,48 @@ public function profile(): Profile
{
return Catalog::resolve($this);
}

public function impersonateTarget(): ?string
{
return match ($this) {
self::Chrome99 => 'chrome99',
self::Chrome100 => 'chrome100',
self::Chrome101 => 'chrome101',
self::Chrome104 => 'chrome104',
self::Chrome107 => 'chrome107',
self::Chrome110 => 'chrome110',
self::Chrome116 => 'chrome116',
self::Chrome119 => 'chrome119',
self::Chrome120 => 'chrome120',
self::Chrome123 => 'chrome123',
self::Chrome124 => 'chrome124',
self::Chrome131 => 'chrome131',
self::Chrome133a => 'chrome133a',
self::Chrome136 => 'chrome136',
self::Chrome142 => 'chrome142',
self::Chrome145 => 'chrome145',
self::Chrome146 => 'chrome146',
self::Chrome99Android => 'chrome99_android',
self::Chrome131Android => 'chrome131_android',
self::Firefox133 => 'firefox133',
self::Firefox135 => 'firefox135',
self::Firefox144 => 'firefox144',
self::Firefox147 => 'firefox147',
self::Safari153 => 'safari153',
self::Safari155 => 'safari155',
self::Safari170 => 'safari170',
self::Safari172iOS => 'safari172_ios',
self::Safari18 => 'safari180',
self::SafariIPad18 => 'safari180_ios',
self::SafariIOS18 => 'safari180_ios',
self::Safari184 => 'safari184',
self::Safari184iOS => 'safari184_ios',
self::Safari260 => 'safari260',
self::Safari260iOS => 'safari260_ios',
self::Edge99 => 'edge99',
self::Edge101 => 'edge101',
self::Tor145 => 'tor145',
default => null,
};
}
}
1 change: 1 addition & 0 deletions src/Emulation/Catalog.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public static function resolve(Browser $browser): Profile
defaultHeaders: $profile->defaultHeaders,
originalHeaderMap: $profile->originalHeaderMap,
connectionGroup: ConnectionGroup::named($browser->value),
impersonateTarget: $browser->impersonateTarget(),
);
}
}
4 changes: 0 additions & 4 deletions src/Emulation/Catalog/Chrome.php
Original file line number Diff line number Diff line change
Expand Up @@ -427,9 +427,7 @@ private static function baseHttp2Options(): Http2Options
return Http2Options::builder()
->headerTableSize(65536)
->enablePush(false)
->maxConcurrentStreams(1000)
->initialWindowSize(6291456)
->maxFrameSize(16384)
->maxHeaderListSize(262144)
->initialConnWindowSize(15663105)
->headersPseudoOrder(new PseudoHeaderOrder([
Expand All @@ -441,9 +439,7 @@ private static function baseHttp2Options(): Http2Options
->settingsOrder(new SettingsOrder([
SettingId::HeaderTableSize,
SettingId::EnablePush,
SettingId::MaxConcurrentStreams,
SettingId::InitialWindowSize,
SettingId::MaxFrameSize,
SettingId::MaxHeaderListSize,
]))
->build();
Expand Down
4 changes: 0 additions & 4 deletions src/Emulation/Catalog/Firefox.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,8 @@ private static function baseHttp2Options(): Http2Options
return Http2Options::builder()
->headerTableSize(65536)
->enablePush(false)
->maxConcurrentStreams(100)
->initialWindowSize(131072)
->maxFrameSize(16384)
->maxHeaderListSize(65536)
->initialConnWindowSize(12582912)
->headersPseudoOrder(new PseudoHeaderOrder([
PseudoHeader::Method,
Expand All @@ -140,10 +138,8 @@ private static function baseHttp2Options(): Http2Options
->settingsOrder(new SettingsOrder([
SettingId::HeaderTableSize,
SettingId::EnablePush,
SettingId::MaxConcurrentStreams,
SettingId::InitialWindowSize,
SettingId::MaxFrameSize,
SettingId::MaxHeaderListSize,
]))
->build();
}
Expand Down
147 changes: 52 additions & 95 deletions src/Emulation/Catalog/Safari.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@

final class Safari
{
// wreq cipher list (23 ciphers — with CBC SHA384/SHA256 but NO 3DES)
private const string CIPHER_LIST = 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA';

// curl_impersonate Safari 15.3 cipher list (26 ciphers — with SHA384/SHA256 AND 3DES)
private const string CI_CIPHER_LIST_15_3 = 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:DES-CBC3-SHA';

Expand All @@ -34,34 +31,49 @@ final class Safari

private const string CURVES_LIST = 'X25519:P-256:P-384:P-521';

private const string SIGALGS_LIST = 'ecdsa_secp256r1_sha256:rsa_pss_rsae_sha256:rsa_pkcs1_sha256:ecdsa_secp384r1_sha384:rsa_pss_rsae_sha384:rsa_pkcs1_sha384:rsa_pss_rsae_sha512:rsa_pkcs1_sha512:rsa_pkcs1_sha1';

// curl_impersonate sigalgs for Safari 15.x-17.x (includes ecdsa_sha1 + duplicate rsa_pss_rsae_sha384)
private const string CI_SIGALGS_LEGACY = 'ecdsa_secp256r1_sha256:rsa_pss_rsae_sha256:rsa_pkcs1_sha256:ecdsa_secp384r1_sha384:ecdsa_sha1:rsa_pss_rsae_sha384:rsa_pss_rsae_sha384:rsa_pkcs1_sha384:rsa_pss_rsae_sha512:rsa_pkcs1_sha512:rsa_pkcs1_sha1';

// curl_impersonate sigalgs for Safari 18.x+ (no ecdsa_sha1, has duplicate rsa_pss_rsae_sha384)
private const string CI_SIGALGS_MODERN = 'ecdsa_secp256r1_sha256:rsa_pss_rsae_sha256:rsa_pkcs1_sha256:ecdsa_secp384r1_sha384:rsa_pss_rsae_sha384:rsa_pss_rsae_sha384:rsa_pkcs1_sha384:rsa_pss_rsae_sha512:rsa_pkcs1_sha512:rsa_pkcs1_sha1';

// ─── wreq profiles (v18, iPad18, iOS18) — unchanged ───

// Safari 18.0 — uses CI profile to match curl_safari180 binary
public static function v18(): Profile
{
return self::buildProfile(
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15',
return new Profile(
tlsOptions: self::ciTls18x(),
http2Options: self::ciHttp2_180(),
defaultHeaders: self::ciHeaders18x(
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15',
encoding: 'gzip, deflate, br',
),
originalHeaderMap: self::ciHeaderOrder18x(),
);
}

public static function iPad18(): Profile
{
return self::buildProfile(
userAgent: 'Mozilla/5.0 (iPad; CPU OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1',
return new Profile(
tlsOptions: self::ciTls18x(),
http2Options: self::ciHttp2_180(),
defaultHeaders: self::ciHeaders18x(
userAgent: 'Mozilla/5.0 (iPad; CPU OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1',
encoding: 'gzip, deflate, br',
),
originalHeaderMap: self::ciHeaderOrder18x(),
);
}

public static function iOS18(): Profile
{
return self::buildProfile(
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',
return new Profile(
tlsOptions: self::ciTls18x(),
http2Options: self::ciHttp2_180(),
defaultHeaders: self::ciHeaders18x(
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',
encoding: 'gzip, deflate, br',
),
originalHeaderMap: self::ciHeaderOrder18x(),
);
}

Expand Down Expand Up @@ -173,88 +185,6 @@ public static function v260iOS(): Profile
);
}

// ─── wreq private helpers (unchanged) ───

private static function baseTlsOptions(): TlsOptions
{
return TlsOptions::builder()
->minTlsVersion(TlsVersion::TLS_1_2)
->maxTlsVersion(TlsVersion::TLS_1_3)
->cipherList(self::CIPHER_LIST)
->sigalgsList(self::SIGALGS_LIST)
->curvesList(self::CURVES_LIST)
->alpnProtocols([AlpnProtocol::Http2, AlpnProtocol::Http1])
->keyShares([KeyShare::X25519])
->enableOcspStapling(true)
->enableSignedCertTimestamps(false)
->greaseEnabled(false)
->permuteExtensions(false)
->enableEchGrease(false)
->sessionTicket(true)
->build();
}

private static function baseHttp2Options(): Http2Options
{
return Http2Options::builder()
->headerTableSize(4096)
->enablePush(false)
->maxConcurrentStreams(100)
->initialWindowSize(2097152)
->maxFrameSize(16384)
->initialConnWindowSize(10485760)
->headersPseudoOrder(new PseudoHeaderOrder([
PseudoHeader::Method,
PseudoHeader::Scheme,
PseudoHeader::Path,
PseudoHeader::Authority,
]))
->settingsOrder(new SettingsOrder([
SettingId::HeaderTableSize,
SettingId::EnablePush,
SettingId::MaxConcurrentStreams,
SettingId::InitialWindowSize,
SettingId::MaxFrameSize,
]))
->build();
}

/**
* @return array<string, string>
*/
private static function baseHeaders(string $userAgent): array
{
return [
'User-Agent' => $userAgent,
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' => 'en-US,en;q=0.9',
'Accept-Encoding' => 'gzip, deflate, br',
];
}

private static function baseHeaderOrder(): OriginalHeaderMap
{
return new OriginalHeaderMap([
'Host',
'Accept',
'User-Agent',
'Accept-Language',
'Accept-Encoding',
'Connection',
'Cookie',
]);
}

private static function buildProfile(string $userAgent): Profile
{
return new Profile(
tlsOptions: self::baseTlsOptions(),
http2Options: self::baseHttp2Options(),
defaultHeaders: self::baseHeaders($userAgent),
originalHeaderMap: self::baseHeaderOrder(),
);
}

// ─── curl_impersonate private helpers ───

// TLS: Safari 15.x-17.x (TLS 1.0, no session ticket, GREASE, signedCertTS)
Expand Down Expand Up @@ -355,6 +285,33 @@ private static function ciHttp2Legacy(bool $enablePush, int $windowSize): Http2O
return $builder->build();
}

// HTTP/2: Safari 18.0 (2:0;3:100;4:2097152;8:1;9:1, pseudo msap, weight 256)
private static function ciHttp2_180(): Http2Options
{
return Http2Options::builder()
->enablePush(false)
->maxConcurrentStreams(100)
->initialWindowSize(2097152)
->enableConnectProtocol(true)
->noRfc7540Priorities(true)
->initialConnWindowSize(10420225)
->headersStreamDependency(new StreamDependency(0, 256, false))
->headersPseudoOrder(new PseudoHeaderOrder([
PseudoHeader::Method,
PseudoHeader::Scheme,
PseudoHeader::Authority,
PseudoHeader::Path,
]))
->settingsOrder(new SettingsOrder([
SettingId::EnablePush,
SettingId::MaxConcurrentStreams,
SettingId::InitialWindowSize,
SettingId::EnableConnectProtocol,
SettingId::NoRfc7540Priorities,
]))
->build();
}

// HTTP/2: Safari 18.4 (2:0;3:100;4:2097152;9:1, pseudo msap, weight 256)
private static function ciHttp2_184(): Http2Options
{
Expand Down
1 change: 1 addition & 0 deletions src/Emulation/Profile.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function __construct(
public array $defaultHeaders = [],
public ?OriginalHeaderMap $originalHeaderMap = null,
public ?ConnectionGroup $connectionGroup = null,
public ?string $impersonateTarget = null,
) {}

public function tlsOptions(): ?TlsOptions
Expand Down
30 changes: 27 additions & 3 deletions src/Transport/ProcessTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function __construct(

public function send(RequestInterface $request, Profile $profile, TransportOptions $options): ResponseInterface
{
$binary = $this->binaryPath ?? self::detectBinaryPath();
$binary = $this->resolveBinary($profile);

if ($binary === null) {
throw new TransportException('curl_impersonate binary not found. Install curl-impersonate or set the binary path.'); // @codeCoverageIgnore
Expand All @@ -30,8 +30,8 @@ public function send(RequestInterface $request, Profile $profile, TransportOptio
$args = $this->buildArguments($request, $profile, $options);
$command = escapeshellcmd($binary).' '.implode(' ', array_map(escapeshellarg(...), $args));

// Include response headers with -D - and suppress progress meter
$command .= ' -s -D -';
// -s: suppress progress meter, -S: show errors in stderr, -D -: dump headers to stdout
$command .= ' -sS -D -';

$process = proc_open(
$command,
Expand Down Expand Up @@ -78,6 +78,30 @@ public function supportsHttp2Configuration(): bool
return true;
}

private function resolveBinary(Profile $profile): ?string
{
if ($this->binaryPath !== null) {
return $this->binaryPath;
}

if ($profile->impersonateTarget !== null) {
$targetBinary = 'curl_'.$profile->impersonateTarget;

$envPath = getenv('REQXIDE_CURL_IMPERSONATE_DIR');
if ($envPath !== false && is_executable($envPath.'/'.$targetBinary)) {
return $envPath.'/'.$targetBinary;
}

foreach (['/usr/local/bin', '/usr/bin'] as $dir) {
if (is_executable($dir.'/'.$targetBinary)) {
return $dir.'/'.$targetBinary;
}
}
}

return self::detectBinaryPath();
}

public static function detectBinaryPath(): ?string
{
$envPath = getenv('REQXIDE_CURL_IMPERSONATE_PATH');
Expand Down
6 changes: 2 additions & 4 deletions tests/Unit/Emulation/Catalog/ChromeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
expect($http2?->initialWindowSize)->toBe(6291456)
->and($http2?->headerTableSize)->toBe(65536)
->and($http2?->enablePush)->toBeFalse()
->and($http2?->maxConcurrentStreams)->toBe(1000)
->and($http2?->maxFrameSize)->toBe(16384)
->and($http2?->maxConcurrentStreams)->toBeNull()
->and($http2?->maxFrameSize)->toBeNull()
->and($http2?->maxHeaderListSize)->toBe(262144)
->and($http2?->initialConnWindowSize)->toBe(15663105);
});
Expand All @@ -78,9 +78,7 @@
expect($profile->http2Options?->settingsOrder?->settings)->toBe([
SettingId::HeaderTableSize,
SettingId::EnablePush,
SettingId::MaxConcurrentStreams,
SettingId::InitialWindowSize,
SettingId::MaxFrameSize,
SettingId::MaxHeaderListSize,
]);
});
Expand Down
Loading
Loading