├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin ├── curl-impersonate-chrome ├── curl-impersonate-ff ├── curl_chrome100 ├── curl_chrome101 ├── curl_chrome104 ├── curl_chrome107 ├── curl_chrome110 ├── curl_chrome116 ├── curl_chrome99 ├── curl_chrome99_android ├── curl_edge101 ├── curl_edge99 ├── curl_ff100 ├── curl_ff102 ├── curl_ff109 ├── curl_ff117 ├── curl_ff91esr ├── curl_ff95 ├── curl_ff98 ├── curl_safari15_3 └── curl_safari15_5 ├── composer.json └── src ├── Browser ├── Browser.php └── BrowserInterface.php ├── ClientInterface.php ├── Exception └── RequestException.php ├── PHPImpersonate.php ├── PHPImpersonateFactory.php ├── Request.php ├── Response.php └── utils.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `php-impersonate` will be documented in this file. 4 | 5 | ## v1.0.0 - 2025-02-26 6 | 7 | Release v1.0.0 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) hamaadraza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP-Impersonate 2 | 3 | [![Tests](https://img.shields.io/github/actions/workflow/status/hamaadraza/php-impersonate/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/hamaadraza/php-impersonate/actions/workflows/run-tests.yml) 4 | 5 | A PHP library for making HTTP requests with browser impersonation. This library uses curl-impersonate to mimic various browsers' network signatures, making it useful for accessing websites that may detect and block automated requests. 6 | 7 | ## Platform Requirements 8 | 9 | **IMPORTANT**: This package only works on Linux platforms. Windows and macOS are not supported due to the reliance on Linux-specific binary dependencies. 10 | 11 | ## Installation 12 | 13 | Install via Composer: 14 | 15 | ```bash 16 | composer require hamaadraza/php-impersonate 17 | ``` 18 | 19 | ## System Requirements 20 | 21 | - PHP 8.0 or higher 22 | - Linux operating system 23 | 24 | ## Basic Usage 25 | 26 | ```php 27 | body(); 35 | 36 | // POST request with data 37 | $response = PHPImpersonate::post('https://example.com/api', [ 38 | 'username' => 'johndoe', 39 | 'email' => 'john@example.com' 40 | ]); 41 | 42 | // Check the response 43 | if ($response->isSuccess()) { 44 | $data = $response->json(); 45 | echo "User created with ID: " . $data['id']; 46 | } else { 47 | echo "Error: " . $response->status(); 48 | } 49 | ``` 50 | 51 | ## API Reference 52 | 53 | ### Static Methods 54 | 55 | The library provides convenient static methods for making requests: 56 | 57 | ```php 58 | // GET request with optional headers and timeout 59 | PHPImpersonate::get(string $url, array $headers = [], int $timeout = 30): Response 60 | 61 | // POST request with optional data, headers and timeout 62 | PHPImpersonate::post(string $url, ?array $data = null, array $headers = [], int $timeout = 30): Response 63 | 64 | // PUT request with optional data, headers and timeout 65 | PHPImpersonate::put(string $url, ?array $data = null, array $headers = [], int $timeout = 30): Response 66 | 67 | // PATCH request with optional data, headers and timeout 68 | PHPImpersonate::patch(string $url, ?array $data = null, array $headers = [], int $timeout = 30): Response 69 | 70 | // DELETE request with optional headers and timeout 71 | PHPImpersonate::delete(string $url, array $headers = [], int $timeout = 30): Response 72 | 73 | // HEAD request with optional headers and timeout 74 | PHPImpersonate::head(string $url, array $headers = [], int $timeout = 30): Response 75 | ``` 76 | 77 | ### Instance Methods 78 | 79 | You can also create an instance of the client for more configuration options: 80 | 81 | ```php 82 | // Create a client with specific browser and timeout 83 | $client = new PHPImpersonate('chrome107', 30); 84 | 85 | // Instance methods 86 | $client->sendGet(string $url, array $headers = []): Response 87 | $client->sendPost(string $url, ?array $data = null, array $headers = []): Response 88 | $client->sendPut(string $url, ?array $data = null, array $headers = []): Response 89 | $client->sendPatch(string $url, ?array $data = null, array $headers = []): Response 90 | $client->sendDelete(string $url, array $headers = []): Response 91 | $client->sendHead(string $url, array $headers = []): Response 92 | 93 | // Generic send method 94 | $client->send(Request $request): Response 95 | ``` 96 | 97 | ### Response Methods 98 | 99 | The `Response` class provides several methods for working with HTTP responses: 100 | 101 | ```php 102 | // Get the HTTP status code 103 | $response->status(): int 104 | 105 | // Get the response body as string 106 | $response->body(): string 107 | 108 | // Check if the response was successful (status code 200-299) 109 | $response->isSuccess(): bool 110 | 111 | // Parse the JSON response 112 | $response->json(): array|null 113 | 114 | // Get a specific header value 115 | $response->header(string $name, ?string $default = null): ?string 116 | 117 | // Get all headers 118 | $response->headers(): array 119 | 120 | // Dump information about the response (returns string) 121 | $response->dump(): string 122 | 123 | // Output debug information about the response (echoes and returns self) 124 | $response->debug(): Response 125 | ``` 126 | 127 | ## Browser Options 128 | 129 | PHP-Impersonate supports mimicking various browsers: 130 | 131 | - `chrome99_android` (default) 132 | - `chrome99` 133 | - `chrome100` 134 | - `chrome101` 135 | - `chrome104` 136 | - `chrome107` 137 | - `chrome110` 138 | - `chrome116` 139 | - `edge99` 140 | - `edge101` 141 | - `ff91esr` 142 | - `ff95` 143 | - `ff98` 144 | - `ff100` 145 | - `ff102` 146 | - `ff109` 147 | - `ff117` 148 | - `safari15_3` 149 | - `safari15_5` 150 | 151 | Example: 152 | ```php 153 | // Create a client that mimics Firefox 154 | $client = new PHPImpersonate('firefox105'); 155 | $response = $client->sendGet('https://example.com'); 156 | ``` 157 | 158 | ## Timeouts 159 | 160 | You can configure request timeouts: 161 | 162 | ```php 163 | // Set a 5-second timeout for this request 164 | $response = PHPImpersonate::get('https://example.com', [], 5); 165 | 166 | // Or when creating a client instance 167 | $client = new PHPImpersonate('chrome107', 10); // 10-second timeout 168 | ``` 169 | 170 | ## Advanced Examples 171 | 172 | ### JSON API Request 173 | 174 | ```php 175 | // Data will be automatically converted to JSON with correct Content-Type 176 | $data = [ 177 | 'title' => 'New Post', 178 | 'body' => 'This is the content', 179 | 'userId' => 1 180 | ]; 181 | 182 | $response = PHPImpersonate::post( 183 | 'https://jsonplaceholder.typicode.com/posts', 184 | $data, 185 | ['Content-Type' => 'application/json'] 186 | ); 187 | 188 | $post = $response->json(); 189 | echo "Created post with ID: {$post['id']}\n"; 190 | ``` 191 | 192 | ### Error Handling 193 | 194 | ```php 195 | try { 196 | $response = PHPImpersonate::get('https://example.com/nonexistent', [], 5); 197 | 198 | if (!$response->isSuccess()) { 199 | echo "Error: HTTP {$response->status()}\n"; 200 | echo $response->body(); 201 | } 202 | } catch (\Raza\PHPImpersonate\Exception\RequestException $e) { 203 | echo "Request failed: " . $e->getMessage(); 204 | } 205 | ``` 206 | 207 | ## Testing 208 | 209 | Run the test suite: 210 | 211 | ```bash 212 | composer test 213 | ``` 214 | 215 | ## License 216 | 217 | This project is licensed under the MIT License - see the LICENSE file for details. 218 | 219 | ## Data Formats for POST, PUT and PATCH Requests 220 | 221 | PHP-Impersonate supports sending data in different formats: 222 | 223 | ### Form Data 224 | 225 | By default, data is sent as form data (`application/x-www-form-urlencoded`): 226 | 227 | ```php 228 | // This will be sent as form data 229 | $response = PHPImpersonate::post('https://example.com/api', [ 230 | 'username' => 'johndoe', 231 | 'email' => 'john@example.com' 232 | ]); 233 | 234 | // Explicitly specify form data 235 | $response = PHPImpersonate::post('https://example.com/api', 236 | [ 237 | 'username' => 'johndoe', 238 | 'email' => 'john@example.com' 239 | ], 240 | ['Content-Type' => 'application/x-www-form-urlencoded'] 241 | ); 242 | ``` 243 | 244 | ### JSON Data 245 | 246 | You can send data as JSON by specifying the `Content-Type` header: 247 | 248 | ```php 249 | // Send data as JSON 250 | $response = PHPImpersonate::post('https://example.com/api', 251 | [ 252 | 'username' => 'johndoe', 253 | 'email' => 'john@example.com' 254 | ], 255 | ['Content-Type' => 'application/json'] 256 | ); 257 | ``` 258 | 259 | For PUT and PATCH requests, JSON is used as the default format. 260 | -------------------------------------------------------------------------------- /bin/curl-impersonate-chrome: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamaadraza/php-impersonate/7fbbd53f6e538fcfb1216fbb8a686c1b3736fe51/bin/curl-impersonate-chrome -------------------------------------------------------------------------------- /bin/curl-impersonate-ff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamaadraza/php-impersonate/7fbbd53f6e538fcfb1216fbb8a686c1b3736fe51/bin/curl-impersonate-ff -------------------------------------------------------------------------------- /bin/curl_chrome100: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using this reference 8 | # https://wiki.mozilla.org/Security/Cipher_Suites 9 | "$dir/curl-impersonate-chrome" \ 10 | --ciphers TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,ECDHE-RSA-AES128-SHA,ECDHE-RSA-AES256-SHA,AES128-GCM-SHA256,AES256-GCM-SHA384,AES128-SHA,AES256-SHA \ 11 | -H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"' \ 12 | -H 'sec-ch-ua-mobile: ?0' \ 13 | -H 'sec-ch-ua-platform: "Windows"' \ 14 | -H 'Upgrade-Insecure-Requests: 1' \ 15 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36' \ 16 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \ 17 | -H 'Sec-Fetch-Site: none' \ 18 | -H 'Sec-Fetch-Mode: navigate' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'Sec-Fetch-Dest: document' \ 21 | -H 'Accept-Encoding: gzip, deflate, br' \ 22 | -H 'Accept-Language: en-US,en;q=0.9' \ 23 | --http2 --compressed \ 24 | --tlsv1.2 --alps \ 25 | --cert-compression brotli \ 26 | "$@" 27 | -------------------------------------------------------------------------------- /bin/curl_chrome101: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using this reference 8 | # https://wiki.mozilla.org/Security/Cipher_Suites 9 | "$dir/curl-impersonate-chrome" \ 10 | --ciphers TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,ECDHE-RSA-AES128-SHA,ECDHE-RSA-AES256-SHA,AES128-GCM-SHA256,AES256-GCM-SHA384,AES128-SHA,AES256-SHA \ 11 | -H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101"' \ 12 | -H 'sec-ch-ua-mobile: ?0' \ 13 | -H 'sec-ch-ua-platform: "Windows"' \ 14 | -H 'Upgrade-Insecure-Requests: 1' \ 15 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36' \ 16 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \ 17 | -H 'Sec-Fetch-Site: none' \ 18 | -H 'Sec-Fetch-Mode: navigate' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'Sec-Fetch-Dest: document' \ 21 | -H 'Accept-Encoding: gzip, deflate, br' \ 22 | -H 'Accept-Language: en-US,en;q=0.9' \ 23 | --http2 --compressed \ 24 | --tlsv1.2 --alps \ 25 | --cert-compression brotli \ 26 | "$@" 27 | -------------------------------------------------------------------------------- /bin/curl_chrome104: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using this reference 8 | # https://wiki.mozilla.org/Security/Cipher_Suites 9 | "$dir/curl-impersonate-chrome" \ 10 | --ciphers TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,ECDHE-RSA-AES128-SHA,ECDHE-RSA-AES256-SHA,AES128-GCM-SHA256,AES256-GCM-SHA384,AES128-SHA,AES256-SHA \ 11 | -H 'sec-ch-ua: "Chromium";v="104", " Not A;Brand";v="99", "Google Chrome";v="104"' \ 12 | -H 'sec-ch-ua-mobile: ?0' \ 13 | -H 'sec-ch-ua-platform: "Windows"' \ 14 | -H 'Upgrade-Insecure-Requests: 1' \ 15 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36' \ 16 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \ 17 | -H 'Sec-Fetch-Site: none' \ 18 | -H 'Sec-Fetch-Mode: navigate' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'Sec-Fetch-Dest: document' \ 21 | -H 'Accept-Encoding: gzip, deflate, br' \ 22 | -H 'Accept-Language: en-US,en;q=0.9' \ 23 | --http2 --compressed \ 24 | --tlsv1.2 --alps \ 25 | --cert-compression brotli \ 26 | "$@" 27 | -------------------------------------------------------------------------------- /bin/curl_chrome107: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using this reference 8 | # https://wiki.mozilla.org/Security/Cipher_Suites 9 | "$dir/curl-impersonate-chrome" \ 10 | --ciphers TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,ECDHE-RSA-AES128-SHA,ECDHE-RSA-AES256-SHA,AES128-GCM-SHA256,AES256-GCM-SHA384,AES128-SHA,AES256-SHA \ 11 | -H 'sec-ch-ua: "Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"' \ 12 | -H 'sec-ch-ua-mobile: ?0' \ 13 | -H 'sec-ch-ua-platform: "Windows"' \ 14 | -H 'Upgrade-Insecure-Requests: 1' \ 15 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36' \ 16 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \ 17 | -H 'Sec-Fetch-Site: none' \ 18 | -H 'Sec-Fetch-Mode: navigate' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'Sec-Fetch-Dest: document' \ 21 | -H 'Accept-Encoding: gzip, deflate, br' \ 22 | -H 'Accept-Language: en-US,en;q=0.9' \ 23 | --http2 --http2-no-server-push --compressed \ 24 | --tlsv1.2 --alps \ 25 | --cert-compression brotli \ 26 | "$@" 27 | -------------------------------------------------------------------------------- /bin/curl_chrome110: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using this reference 8 | # https://wiki.mozilla.org/Security/Cipher_Suites 9 | "$dir/curl-impersonate-chrome" \ 10 | --ciphers TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,ECDHE-RSA-AES128-SHA,ECDHE-RSA-AES256-SHA,AES128-GCM-SHA256,AES256-GCM-SHA384,AES128-SHA,AES256-SHA \ 11 | -H 'sec-ch-ua: "Chromium";v="110", "Not A(Brand";v="24", "Google Chrome";v="110"' \ 12 | -H 'sec-ch-ua-mobile: ?0' \ 13 | -H 'sec-ch-ua-platform: "Windows"' \ 14 | -H 'Upgrade-Insecure-Requests: 1' \ 15 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36' \ 16 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \ 17 | -H 'Sec-Fetch-Site: none' \ 18 | -H 'Sec-Fetch-Mode: navigate' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'Sec-Fetch-Dest: document' \ 21 | -H 'Accept-Encoding: gzip, deflate, br' \ 22 | -H 'Accept-Language: en-US,en;q=0.9' \ 23 | --http2 --http2-no-server-push --compressed \ 24 | --tlsv1.2 --alps --tls-permute-extensions \ 25 | --cert-compression brotli \ 26 | "$@" 27 | -------------------------------------------------------------------------------- /bin/curl_chrome116: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using this reference 8 | # https://wiki.mozilla.org/Security/Cipher_Suites 9 | "$dir/curl-impersonate-chrome" \ 10 | --ciphers TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,ECDHE-RSA-AES128-SHA,ECDHE-RSA-AES256-SHA,AES128-GCM-SHA256,AES256-GCM-SHA384,AES128-SHA,AES256-SHA \ 11 | -H 'sec-ch-ua: "Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"' \ 12 | -H 'sec-ch-ua-mobile: ?0' \ 13 | -H 'sec-ch-ua-platform: "Windows"' \ 14 | -H 'Upgrade-Insecure-Requests: 1' \ 15 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36' \ 16 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \ 17 | -H 'Sec-Fetch-Site: none' \ 18 | -H 'Sec-Fetch-Mode: navigate' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'Sec-Fetch-Dest: document' \ 21 | -H 'Accept-Encoding: gzip, deflate, br' \ 22 | -H 'Accept-Language: en-US,en;q=0.9' \ 23 | --http2 --http2-no-server-push --compressed \ 24 | --tlsv1.2 --alps --tls-permute-extensions \ 25 | --cert-compression brotli \ 26 | "$@" 27 | -------------------------------------------------------------------------------- /bin/curl_chrome99: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using this reference 8 | # https://wiki.mozilla.org/Security/Cipher_Suites 9 | "$dir/curl-impersonate-chrome" \ 10 | --ciphers TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,ECDHE-RSA-AES128-SHA,ECDHE-RSA-AES256-SHA,AES128-GCM-SHA256,AES256-GCM-SHA384,AES128-SHA,AES256-SHA \ 11 | -H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="99", "Google Chrome";v="99"' \ 12 | -H 'sec-ch-ua-mobile: ?0' \ 13 | -H 'sec-ch-ua-platform: "Windows"' \ 14 | -H 'Upgrade-Insecure-Requests: 1' \ 15 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36' \ 16 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \ 17 | -H 'Sec-Fetch-Site: none' \ 18 | -H 'Sec-Fetch-Mode: navigate' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'Sec-Fetch-Dest: document' \ 21 | -H 'Accept-Encoding: gzip, deflate, br' \ 22 | -H 'Accept-Language: en-US,en;q=0.9' \ 23 | --http2 --compressed \ 24 | --tlsv1.2 --alps \ 25 | --cert-compression brotli \ 26 | "$@" 27 | -------------------------------------------------------------------------------- /bin/curl_chrome99_android: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using this reference 8 | # https://wiki.mozilla.org/Security/Cipher_Suites 9 | "$dir/curl-impersonate-chrome" \ 10 | --ciphers TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,ECDHE-RSA-AES128-SHA,ECDHE-RSA-AES256-SHA,AES128-GCM-SHA256,AES256-GCM-SHA384,AES128-SHA,AES256-SHA \ 11 | -H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="99", "Google Chrome";v="99"' \ 12 | -H 'sec-ch-ua-mobile: ?1' \ 13 | -H 'sec-ch-ua-platform: "Android"' \ 14 | -H 'Upgrade-Insecure-Requests: 1' \ 15 | -H 'User-Agent: Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.58 Mobile Safari/537.36' \ 16 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \ 17 | -H 'Sec-Fetch-Site: none' \ 18 | -H 'Sec-Fetch-Mode: navigate' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'Sec-Fetch-Dest: document' \ 21 | -H 'Accept-Encoding: gzip, deflate, br' \ 22 | -H 'Accept-Language: en-US,en;q=0.9' \ 23 | --http2 --compressed \ 24 | --tlsv1.2 --alps \ 25 | --cert-compression brotli \ 26 | "$@" 27 | -------------------------------------------------------------------------------- /bin/curl_edge101: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using this reference 8 | # https://wiki.mozilla.org/Security/Cipher_Suites 9 | "$dir/curl-impersonate-chrome" \ 10 | --ciphers TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,ECDHE-RSA-AES128-SHA,ECDHE-RSA-AES256-SHA,AES128-GCM-SHA256,AES256-GCM-SHA384,AES128-SHA,AES256-SHA \ 11 | -H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="101", "Microsoft Edge";v="101"' \ 12 | -H 'sec-ch-ua-mobile: ?0' \ 13 | -H 'sec-ch-ua-platform: "Windows"' \ 14 | -H 'Upgrade-Insecure-Requests: 1' \ 15 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.47' \ 16 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \ 17 | -H 'Sec-Fetch-Site: none' \ 18 | -H 'Sec-Fetch-Mode: navigate' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'Sec-Fetch-Dest: document' \ 21 | -H 'Accept-Encoding: gzip, deflate, br' \ 22 | -H 'Accept-Language: en-US,en;q=0.9' \ 23 | --http2 --compressed \ 24 | --tlsv1.2 --alps \ 25 | --cert-compression brotli \ 26 | "$@" 27 | -------------------------------------------------------------------------------- /bin/curl_edge99: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using this reference 8 | # https://wiki.mozilla.org/Security/Cipher_Suites 9 | "$dir/curl-impersonate-chrome" \ 10 | --ciphers TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,ECDHE-RSA-AES128-SHA,ECDHE-RSA-AES256-SHA,AES128-GCM-SHA256,AES256-GCM-SHA384,AES128-SHA,AES256-SHA \ 11 | -H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="99", "Microsoft Edge";v="99"' \ 12 | -H 'sec-ch-ua-mobile: ?0' \ 13 | -H 'sec-ch-ua-platform: "Windows"' \ 14 | -H 'Upgrade-Insecure-Requests: 1' \ 15 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.30' \ 16 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \ 17 | -H 'Sec-Fetch-Site: none' \ 18 | -H 'Sec-Fetch-Mode: navigate' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'Sec-Fetch-Dest: document' \ 21 | -H 'Accept-Encoding: gzip, deflate, br' \ 22 | -H 'Accept-Language: en-US,en;q=0.9' \ 23 | --http2 --compressed \ 24 | --tlsv1.2 --alps \ 25 | --cert-compression brotli \ 26 | "$@" 27 | -------------------------------------------------------------------------------- /bin/curl_ff100: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using the cipherlist array at 8 | # https://github.com/curl/curl/blob/master/lib/vtls/nss.c 9 | "$dir/curl-impersonate-ff" \ 10 | --ciphers aes_128_gcm_sha_256,chacha20_poly1305_sha_256,aes_256_gcm_sha_384,ecdhe_ecdsa_aes_128_gcm_sha_256,ecdhe_rsa_aes_128_gcm_sha_256,ecdhe_ecdsa_chacha20_poly1305_sha_256,ecdhe_rsa_chacha20_poly1305_sha_256,ecdhe_ecdsa_aes_256_gcm_sha_384,ecdhe_rsa_aes_256_gcm_sha_384,ecdhe_ecdsa_aes_256_sha,ecdhe_ecdsa_aes_128_sha,ecdhe_rsa_aes_128_sha,ecdhe_rsa_aes_256_sha,rsa_aes_128_gcm_sha_256,rsa_aes_256_gcm_sha_384,rsa_aes_128_sha,rsa_aes_256_sha \ 11 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:100.0) Gecko/20100101 Firefox/100.0' \ 12 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' \ 13 | -H 'Accept-Language: en-US,en;q=0.5' \ 14 | -H 'Accept-Encoding: gzip, deflate, br' \ 15 | -H 'Upgrade-Insecure-Requests: 1' \ 16 | -H 'Sec-Fetch-Dest: document' \ 17 | -H 'Sec-Fetch-Mode: navigate' \ 18 | -H 'Sec-Fetch-Site: none' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'TE: Trailers' \ 21 | --http2 --compressed \ 22 | "$@" 23 | -------------------------------------------------------------------------------- /bin/curl_ff102: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using the cipherlist array at 8 | # https://github.com/curl/curl/blob/master/lib/vtls/nss.c 9 | "$dir/curl-impersonate-ff" \ 10 | --ciphers aes_128_gcm_sha_256,chacha20_poly1305_sha_256,aes_256_gcm_sha_384,ecdhe_ecdsa_aes_128_gcm_sha_256,ecdhe_rsa_aes_128_gcm_sha_256,ecdhe_ecdsa_chacha20_poly1305_sha_256,ecdhe_rsa_chacha20_poly1305_sha_256,ecdhe_ecdsa_aes_256_gcm_sha_384,ecdhe_rsa_aes_256_gcm_sha_384,ecdhe_ecdsa_aes_256_sha,ecdhe_ecdsa_aes_128_sha,ecdhe_rsa_aes_128_sha,ecdhe_rsa_aes_256_sha,rsa_aes_128_gcm_sha_256,rsa_aes_256_gcm_sha_384,rsa_aes_128_sha,rsa_aes_256_sha \ 11 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0' \ 12 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' \ 13 | -H 'Accept-Language: en-US,en;q=0.5' \ 14 | -H 'Accept-Encoding: gzip, deflate, br' \ 15 | -H 'Upgrade-Insecure-Requests: 1' \ 16 | -H 'Sec-Fetch-Dest: document' \ 17 | -H 'Sec-Fetch-Mode: navigate' \ 18 | -H 'Sec-Fetch-Site: none' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'TE: Trailers' \ 21 | --http2 --compressed \ 22 | "$@" 23 | -------------------------------------------------------------------------------- /bin/curl_ff109: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using the cipherlist array at 8 | # https://github.com/curl/curl/blob/master/lib/vtls/nss.c 9 | "$dir/curl-impersonate-ff" \ 10 | --ciphers aes_128_gcm_sha_256,chacha20_poly1305_sha_256,aes_256_gcm_sha_384,ecdhe_ecdsa_aes_128_gcm_sha_256,ecdhe_rsa_aes_128_gcm_sha_256,ecdhe_ecdsa_chacha20_poly1305_sha_256,ecdhe_rsa_chacha20_poly1305_sha_256,ecdhe_ecdsa_aes_256_gcm_sha_384,ecdhe_rsa_aes_256_gcm_sha_384,ecdhe_ecdsa_aes_256_sha,ecdhe_ecdsa_aes_128_sha,ecdhe_rsa_aes_128_sha,ecdhe_rsa_aes_256_sha,rsa_aes_128_gcm_sha_256,rsa_aes_256_gcm_sha_384,rsa_aes_128_sha,rsa_aes_256_sha \ 11 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0' \ 12 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' \ 13 | -H 'Accept-Language: en-US,en;q=0.5' \ 14 | -H 'Accept-Encoding: gzip, deflate, br' \ 15 | -H 'Upgrade-Insecure-Requests: 1' \ 16 | -H 'Sec-Fetch-Dest: document' \ 17 | -H 'Sec-Fetch-Mode: navigate' \ 18 | -H 'Sec-Fetch-Site: none' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'TE: Trailers' \ 21 | --http2 --compressed \ 22 | "$@" 23 | -------------------------------------------------------------------------------- /bin/curl_ff117: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using the cipherlist array at 8 | # https://github.com/curl/curl/blob/master/lib/vtls/nss.c 9 | "$dir/curl-impersonate-ff" \ 10 | --ciphers aes_128_gcm_sha_256,chacha20_poly1305_sha_256,aes_256_gcm_sha_384,ecdhe_ecdsa_aes_128_gcm_sha_256,ecdhe_rsa_aes_128_gcm_sha_256,ecdhe_ecdsa_chacha20_poly1305_sha_256,ecdhe_rsa_chacha20_poly1305_sha_256,ecdhe_ecdsa_aes_256_gcm_sha_384,ecdhe_rsa_aes_256_gcm_sha_384,ecdhe_ecdsa_aes_256_sha,ecdhe_ecdsa_aes_128_sha,ecdhe_rsa_aes_128_sha,ecdhe_rsa_aes_256_sha,rsa_aes_128_gcm_sha_256,rsa_aes_256_gcm_sha_384,rsa_aes_128_sha,rsa_aes_256_sha \ 11 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/117.0' \ 12 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' \ 13 | -H 'Accept-Language: en-US,en;q=0.5' \ 14 | -H 'Accept-Encoding: gzip, deflate, br' \ 15 | -H 'Upgrade-Insecure-Requests: 1' \ 16 | -H 'Sec-Fetch-Dest: document' \ 17 | -H 'Sec-Fetch-Mode: navigate' \ 18 | -H 'Sec-Fetch-Site: none' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'TE: Trailers' \ 21 | --http2 --compressed \ 22 | "$@" 23 | -------------------------------------------------------------------------------- /bin/curl_ff91esr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using the cipherlist array at 8 | # https://github.com/curl/curl/blob/master/lib/vtls/nss.c 9 | "$dir/curl-impersonate-ff" \ 10 | --ciphers aes_128_gcm_sha_256,chacha20_poly1305_sha_256,aes_256_gcm_sha_384,ecdhe_ecdsa_aes_128_gcm_sha_256,ecdhe_rsa_aes_128_gcm_sha_256,ecdhe_ecdsa_chacha20_poly1305_sha_256,ecdhe_rsa_chacha20_poly1305_sha_256,ecdhe_ecdsa_aes_256_gcm_sha_384,ecdhe_rsa_aes_256_gcm_sha_384,ecdhe_ecdsa_aes_256_sha,ecdhe_ecdsa_aes_128_sha,ecdhe_rsa_aes_128_sha,ecdhe_rsa_aes_256_sha,rsa_aes_128_gcm_sha_256,rsa_aes_256_gcm_sha_384,rsa_aes_128_sha,rsa_aes_256_sha,rsa_3des_ede_cbc_sha \ 11 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0' \ 12 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' \ 13 | -H 'Accept-Language: en-US,en;q=0.5' \ 14 | -H 'Accept-Encoding: gzip, deflate, br' \ 15 | -H 'Upgrade-Insecure-Requests: 1' \ 16 | -H 'Sec-Fetch-Dest: document' \ 17 | -H 'Sec-Fetch-Mode: navigate' \ 18 | -H 'Sec-Fetch-Site: none' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'TE: Trailers' \ 21 | --http2 --compressed \ 22 | "$@" 23 | -------------------------------------------------------------------------------- /bin/curl_ff95: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using the cipherlist array at 8 | # https://github.com/curl/curl/blob/master/lib/vtls/nss.c 9 | "$dir/curl-impersonate-ff" \ 10 | --ciphers aes_128_gcm_sha_256,chacha20_poly1305_sha_256,aes_256_gcm_sha_384,ecdhe_ecdsa_aes_128_gcm_sha_256,ecdhe_rsa_aes_128_gcm_sha_256,ecdhe_ecdsa_chacha20_poly1305_sha_256,ecdhe_rsa_chacha20_poly1305_sha_256,ecdhe_ecdsa_aes_256_gcm_sha_384,ecdhe_rsa_aes_256_gcm_sha_384,ecdhe_ecdsa_aes_256_sha,ecdhe_ecdsa_aes_128_sha,ecdhe_rsa_aes_128_sha,ecdhe_rsa_aes_256_sha,rsa_aes_128_gcm_sha_256,rsa_aes_256_gcm_sha_384,rsa_aes_128_sha,rsa_aes_256_sha \ 11 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0' \ 12 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' \ 13 | -H 'Accept-Language: en-US,en;q=0.5' \ 14 | -H 'Accept-Encoding: gzip, deflate, br' \ 15 | -H 'Upgrade-Insecure-Requests: 1' \ 16 | -H 'Sec-Fetch-Dest: document' \ 17 | -H 'Sec-Fetch-Mode: navigate' \ 18 | -H 'Sec-Fetch-Site: none' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'TE: Trailers' \ 21 | --http2 --compressed \ 22 | "$@" 23 | -------------------------------------------------------------------------------- /bin/curl_ff98: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using the cipherlist array at 8 | # https://github.com/curl/curl/blob/master/lib/vtls/nss.c 9 | "$dir/curl-impersonate-ff" \ 10 | --ciphers aes_128_gcm_sha_256,chacha20_poly1305_sha_256,aes_256_gcm_sha_384,ecdhe_ecdsa_aes_128_gcm_sha_256,ecdhe_rsa_aes_128_gcm_sha_256,ecdhe_ecdsa_chacha20_poly1305_sha_256,ecdhe_rsa_chacha20_poly1305_sha_256,ecdhe_ecdsa_aes_256_gcm_sha_384,ecdhe_rsa_aes_256_gcm_sha_384,ecdhe_ecdsa_aes_256_sha,ecdhe_ecdsa_aes_128_sha,ecdhe_rsa_aes_128_sha,ecdhe_rsa_aes_256_sha,rsa_aes_128_gcm_sha_256,rsa_aes_256_gcm_sha_384,rsa_aes_128_sha,rsa_aes_256_sha \ 11 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0' \ 12 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' \ 13 | -H 'Accept-Language: en-US,en;q=0.5' \ 14 | -H 'Accept-Encoding: gzip, deflate, br' \ 15 | -H 'Upgrade-Insecure-Requests: 1' \ 16 | -H 'Sec-Fetch-Dest: document' \ 17 | -H 'Sec-Fetch-Mode: navigate' \ 18 | -H 'Sec-Fetch-Site: none' \ 19 | -H 'Sec-Fetch-User: ?1' \ 20 | -H 'TE: Trailers' \ 21 | --http2 --compressed \ 22 | "$@" 23 | -------------------------------------------------------------------------------- /bin/curl_safari15_3: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using this reference 8 | # https://wiki.mozilla.org/Security/Cipher_Suites 9 | "$dir/curl-impersonate-chrome" \ 10 | --ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384:TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256:TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384:TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256:TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:TLS_RSA_WITH_AES_256_GCM_SHA384:TLS_RSA_WITH_AES_128_GCM_SHA256:TLS_RSA_WITH_AES_256_CBC_SHA256:TLS_RSA_WITH_AES_128_CBC_SHA256:TLS_RSA_WITH_AES_256_CBC_SHA:TLS_RSA_WITH_AES_128_CBC_SHA:TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA:TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:TLS_RSA_WITH_3DES_EDE_CBC_SHA \ 11 | --curves X25519:P-256:P-384:P-521 \ 12 | --signature-hashes 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 \ 13 | -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Safari/605.1.15' \ 14 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \ 15 | -H 'Accept-Language: en-us' \ 16 | -H 'Accept-Encoding: gzip, deflate, br' \ 17 | --http2 --compressed \ 18 | --tlsv1.0 --no-tls-session-ticket \ 19 | --http2-pseudo-headers-order 'mspa' \ 20 | "$@" 21 | -------------------------------------------------------------------------------- /bin/curl_safari15_5: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find the directory of this script 4 | dir=${0%/*} 5 | 6 | # The list of ciphers can be obtained by looking at the Client Hello message in 7 | # Wireshark, then converting it using this reference 8 | # https://wiki.mozilla.org/Security/Cipher_Suites 9 | "$dir/curl-impersonate-chrome" \ 10 | --ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:TLS_RSA_WITH_AES_256_GCM_SHA384:TLS_RSA_WITH_AES_128_GCM_SHA256:TLS_RSA_WITH_AES_256_CBC_SHA:TLS_RSA_WITH_AES_128_CBC_SHA:TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA:TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:TLS_RSA_WITH_3DES_EDE_CBC_SHA \ 11 | --curves X25519:P-256:P-384:P-521 \ 12 | --signature-hashes 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 \ 13 | -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15' \ 14 | -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \ 15 | -H 'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8' \ 16 | -H 'Accept-Encoding: gzip, deflate, br' \ 17 | --http2 --compressed \ 18 | --tlsv1.0 --no-tls-session-ticket \ 19 | --cert-compression zlib \ 20 | --http2-pseudo-headers-order 'mspa' \ 21 | "$@" 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hamaadraza/php-impersonate", 3 | "version": "1.0.0", 4 | "description": "A PHP library for making HTTP requests with browser impersonation", 5 | "keywords": [ 6 | "hamaadraza", 7 | "php-impersonate" 8 | ], 9 | "homepage": "https://github.com/hamaadraza/php-impersonate", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Hamaad Raza", 14 | "email": "hamaadraza9@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8" 20 | }, 21 | "require-dev": { 22 | "pestphp/pest": "^3.0", 23 | "friendsofphp/php-cs-fixer": "^3.21.1", 24 | "spatie/ray": "^1.28" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Raza\\PHPImpersonate\\": "src" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Raza\\PHPImpersonate\\Tests\\": "tests" 34 | } 35 | }, 36 | "scripts": { 37 | "test": "vendor/bin/pest", 38 | "test-coverage": "vendor/bin/pest --coverage", 39 | "format": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --allow-risky=yes" 40 | }, 41 | "config": { 42 | "sort-packages": true, 43 | "allow-plugins": { 44 | "pestphp/pest-plugin": true, 45 | "phpstan/extension-installer": true 46 | } 47 | }, 48 | "minimum-stability": "dev", 49 | "prefer-stable": true, 50 | "platform": { 51 | "php": ">=8.0", 52 | "os": "linux" 53 | }, 54 | "bin": [ 55 | "bin/curl_chrome99", 56 | "bin/curl_chrome99_android", 57 | "bin/curl_chrome100", 58 | "bin/curl_chrome101", 59 | "bin/curl_chrome104", 60 | "bin/curl_chrome107", 61 | "bin/curl_chrome110", 62 | "bin/curl_chrome116", 63 | "bin/curl_edge99", 64 | "bin/curl_edge101", 65 | "bin/curl_ff91esr", 66 | "bin/curl_ff95", 67 | "bin/curl_ff98", 68 | "bin/curl_ff100", 69 | "bin/curl_ff102", 70 | "bin/curl_ff109", 71 | "bin/curl_ff117", 72 | "bin/curl_safari15_3", 73 | "bin/curl_safari15_5", 74 | "bin/curl-impersonate-chrome", 75 | "bin/curl-impersonate-ff" 76 | ] 77 | } -------------------------------------------------------------------------------- /src/Browser/Browser.php: -------------------------------------------------------------------------------- 1 | resolveExecutablePath(); 19 | } 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | public function getExecutablePath(): string 25 | { 26 | return $this->executablePath; 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | public function getName(): string 33 | { 34 | return $this->name; 35 | } 36 | 37 | /** 38 | * Resolve the executable path for the browser 39 | * 40 | * @throws RuntimeException If the browser is not found 41 | */ 42 | private function resolveExecutablePath(): void 43 | { 44 | $rootPath = getcwd(); 45 | $paths = [ 46 | // Package bin directory 47 | "$rootPath/bin/curl_{$this->name}", 48 | // Vendor bin directory 49 | "$rootPath/vendor/bin/curl_{$this->name}", 50 | // Absolute system path 51 | "/usr/local/bin/curl_{$this->name}", 52 | // Check if it's in PATH 53 | "curl_{$this->name}", 54 | ]; 55 | 56 | foreach ($paths as $path) { 57 | // For absolute paths, check if file exists 58 | if (strpos($path, '/') === 0 && file_exists($path) && is_executable($path)) { 59 | $this->executablePath = $path; 60 | 61 | return; 62 | } 63 | 64 | // For PATH-based executables, use 'which' command 65 | if (strpos($path, '/') !== 0) { 66 | $result = shell_exec("which $path 2>/dev/null"); 67 | if ($result && trim($result) && file_exists(trim($result))) { 68 | $this->executablePath = trim($result); 69 | 70 | return; 71 | } 72 | } 73 | } 74 | 75 | throw new RuntimeException("Browser '{$this->name}' not supported - executable not found. Checked paths: " . implode(", ", $paths)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Browser/BrowserInterface.php: -------------------------------------------------------------------------------- 1 | $headers Headers to send 23 | * @return Response 24 | * @throws RequestException 25 | */ 26 | public function sendGet(string $url, array $headers = []): Response; 27 | 28 | /** 29 | * Send a POST request 30 | * 31 | * @param string $url The URL to request 32 | * @param array|null $data Data to send 33 | * @param array $headers Headers to send 34 | * @return Response 35 | * @throws RequestException 36 | */ 37 | public function sendPost(string $url, ?array $data = null, array $headers = []): Response; 38 | 39 | /** 40 | * Send a HEAD request 41 | * 42 | * @param string $url The URL to request 43 | * @param array $headers Headers to send 44 | * @return Response 45 | * @throws RequestException 46 | */ 47 | public function sendHead(string $url, array $headers = []): Response; 48 | 49 | /** 50 | * Send a DELETE request 51 | * 52 | * @param string $url The URL to request 53 | * @param array $headers Headers to send 54 | * @return Response 55 | * @throws RequestException 56 | */ 57 | public function sendDelete(string $url, array $headers = []): Response; 58 | 59 | /** 60 | * Send a PATCH request 61 | * 62 | * @param string $url The URL to request 63 | * @param array|null $data Data to send 64 | * @param array $headers Headers to send 65 | * @return Response 66 | * @throws RequestException 67 | */ 68 | public function sendPatch(string $url, ?array $data = null, array $headers = []): Response; 69 | 70 | /** 71 | * Send a PUT request 72 | * 73 | * @param string $url The URL to request 74 | * @param array|null $data Data to send 75 | * @param array $headers Headers to send 76 | * @return Response 77 | * @throws RequestException 78 | */ 79 | public function sendPut(string $url, ?array $data = null, array $headers = []): Response; 80 | } 81 | -------------------------------------------------------------------------------- /src/Exception/RequestException.php: -------------------------------------------------------------------------------- 1 | $output Command output (if applicable) 15 | */ 16 | public function __construct( 17 | string $message, 18 | int $code = 0, 19 | ?\Throwable $previous = null, 20 | private ?string $command = null, 21 | private array $output = [] 22 | ) { 23 | parent::__construct($message, $code, $previous); 24 | } 25 | 26 | /** 27 | * Get the command that failed 28 | * 29 | * @return string|null 30 | */ 31 | public function getCommand(): ?string 32 | { 33 | return $this->command; 34 | } 35 | 36 | /** 37 | * Get the command output 38 | * 39 | * @return array 40 | */ 41 | public function getOutput(): array 42 | { 43 | return $this->output; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/PHPImpersonate.php: -------------------------------------------------------------------------------- 1 | $curlOptions Custom curl options 20 | * @throws RequestException If the browser is invalid or platform is not supported 21 | */ 22 | public function __construct( 23 | string|BrowserInterface $browser = self::DEFAULT_BROWSER, 24 | private int $timeout = self::DEFAULT_TIMEOUT, 25 | private array $curlOptions = [] 26 | ) { 27 | // Check if running on Linux 28 | if (PHP_OS !== 'Linux' && strpos(PHP_OS, 'Linux') === false) { 29 | throw new RequestException( 30 | 'PHP-Impersonate requires a Linux operating system. Current OS: ' . PHP_OS 31 | ); 32 | } 33 | 34 | if (is_string($browser)) { 35 | try { 36 | $this->browser = new Browser($browser); 37 | } catch (\RuntimeException $e) { 38 | throw new RequestException($e->getMessage(), 0, $e); 39 | } 40 | } else { 41 | $this->browser = $browser; 42 | } 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | public function send(Request $request): Response 49 | { 50 | $method = $request->getMethod(); 51 | $url = $request->getUrl(); 52 | $headers = $request->getHeaders(); 53 | $body = $request->getBody(); 54 | 55 | $tempFiles = $this->createTempFiles(); 56 | 57 | try { 58 | $command = $this->buildCommand( 59 | $method, 60 | $url, 61 | $tempFiles['body'], 62 | $tempFiles['headers'], 63 | $headers, 64 | $body 65 | ); 66 | 67 | $result = $this->runCommand($command); 68 | 69 | $responseBody = file_exists($tempFiles['body']) 70 | ? (file_get_contents($tempFiles['body']) ?: '') 71 | : ''; 72 | 73 | $statusCode = (int)$result['status_code']; 74 | 75 | $responseHeaders = $this->parseHeaders( 76 | file_exists($tempFiles['headers']) 77 | ? (file_get_contents($tempFiles['headers']) ?: '') 78 | : '' 79 | ); 80 | 81 | return new Response($responseBody, $statusCode, $responseHeaders); 82 | } finally { 83 | $this->cleanupTempFiles($tempFiles); 84 | } 85 | } 86 | 87 | /** 88 | * @inheritDoc 89 | */ 90 | public function sendGet(string $url, array $headers = []): Response 91 | { 92 | return $this->send(Request::get($url, $headers)); 93 | } 94 | 95 | /** 96 | * @inheritDoc 97 | */ 98 | public function sendPost(string $url, ?array $data = null, array $headers = []): Response 99 | { 100 | $headers = $this->normalizeHeaders($headers); 101 | 102 | // Check if JSON content type is specified 103 | $isJson = isset($headers['Content-Type']) && 104 | strpos($headers['Content-Type'], 'application/json') !== false; 105 | 106 | // Encode data appropriately based on Content-Type 107 | $body = null; 108 | if ($data !== null) { 109 | if ($isJson) { 110 | $body = json_encode($data); 111 | } else { 112 | $body = http_build_query($data); 113 | // Set default Content-Type for form data if not specified 114 | if (! isset($headers['Content-Type'])) { 115 | $headers['Content-Type'] = 'application/x-www-form-urlencoded'; 116 | } 117 | } 118 | } 119 | 120 | return $this->send(Request::post($url, $headers, $body)); 121 | } 122 | 123 | /** 124 | * @inheritDoc 125 | */ 126 | public function sendHead(string $url, array $headers = []): Response 127 | { 128 | return $this->send(Request::head($url, $headers)); 129 | } 130 | 131 | /** 132 | * @inheritDoc 133 | */ 134 | public function sendDelete(string $url, array $headers = []): Response 135 | { 136 | return $this->send(Request::delete($url, $headers)); 137 | } 138 | 139 | /** 140 | * @inheritDoc 141 | */ 142 | public function sendPatch(string $url, ?array $data = null, array $headers = []): Response 143 | { 144 | $headers = $this->normalizeHeaders($headers); 145 | 146 | // Check if JSON content type is specified 147 | $isJson = isset($headers['Content-Type']) && 148 | strpos($headers['Content-Type'], 'application/json') !== false; 149 | 150 | // Encode data appropriately 151 | $body = null; 152 | if ($data !== null) { 153 | $body = $isJson ? json_encode($data) : http_build_query($data); 154 | } 155 | 156 | // Add default content type if not specified 157 | if ($data !== null && ! isset($headers['Content-Type'])) { 158 | $headers['Content-Type'] = 'application/json'; 159 | $body = json_encode($data); 160 | } 161 | 162 | return $this->send(Request::patch($url, $headers, $body)); 163 | } 164 | 165 | /** 166 | * @inheritDoc 167 | */ 168 | public function sendPut(string $url, ?array $data = null, array $headers = []): Response 169 | { 170 | $headers = $this->normalizeHeaders($headers); 171 | 172 | // Always use JSON for PUT requests - this is the standard 173 | if ($data !== null) { 174 | $headers['Content-Type'] = 'application/json'; 175 | $body = json_encode($data); 176 | } else { 177 | $body = null; 178 | } 179 | 180 | return $this->send(Request::put($url, $headers, $body)); 181 | } 182 | 183 | /** 184 | * Static version - Get the response from a URL using GET method 185 | * 186 | * @param string $url The URL to request 187 | * @param array $headers Headers to send with the request 188 | * @param int $timeout Timeout in seconds 189 | * @param string $browser Browser to impersonate 190 | * @param array $curlOptions Custom curl options to add to the request 191 | * @return Response 192 | * @throws RequestException 193 | */ 194 | public static function get( 195 | string $url, 196 | array $headers = [], 197 | int $timeout = self::DEFAULT_TIMEOUT, 198 | string $browser = self::DEFAULT_BROWSER, 199 | array $curlOptions = [] 200 | ): Response { 201 | $client = new self($browser, $timeout, $curlOptions); 202 | 203 | return $client->sendGet($url, $headers); 204 | } 205 | 206 | /** 207 | * Static version - Post data to a URL and return response 208 | * 209 | * @param string $url The URL to request 210 | * @param array|null $data Data to send with the POST request 211 | * @param array $headers Headers to send with the request 212 | * @param int $timeout Timeout in seconds 213 | * @param string $browser Browser to impersonate 214 | * @param array $curlOptions Custom curl options to add to the request 215 | * @return Response 216 | * @throws RequestException 217 | */ 218 | public static function post( 219 | string $url, 220 | ?array $data = null, 221 | array $headers = [], 222 | int $timeout = self::DEFAULT_TIMEOUT, 223 | string $browser = self::DEFAULT_BROWSER, 224 | array $curlOptions = [] 225 | ): Response { 226 | $client = new self($browser, $timeout, $curlOptions); 227 | 228 | return $client->sendPost($url, $data, $headers); 229 | } 230 | 231 | /** 232 | * Static version - Get headers and status code for a URL using HEAD request 233 | * 234 | * @param string $url The URL to request 235 | * @param array $headers Headers to send with the request 236 | * @param int $timeout Timeout in seconds 237 | * @param string $browser Browser to impersonate 238 | * @param array $curlOptions Custom curl options to add to the request 239 | * @return Response 240 | * @throws RequestException 241 | */ 242 | public static function head( 243 | string $url, 244 | array $headers = [], 245 | int $timeout = self::DEFAULT_TIMEOUT, 246 | string $browser = self::DEFAULT_BROWSER, 247 | array $curlOptions = [] 248 | ): Response { 249 | $client = new self($browser, $timeout, $curlOptions); 250 | 251 | return $client->sendHead($url, $headers); 252 | } 253 | 254 | /** 255 | * Static version - Delete a resource at a URL 256 | * 257 | * @param string $url The URL to request 258 | * @param array $headers Headers to send with the request 259 | * @param int $timeout Timeout in seconds 260 | * @param string $browser Browser to impersonate 261 | * @param array $curlOptions Custom curl options to add to the request 262 | * @return Response 263 | * @throws RequestException 264 | */ 265 | public static function delete( 266 | string $url, 267 | array $headers = [], 268 | int $timeout = self::DEFAULT_TIMEOUT, 269 | string $browser = self::DEFAULT_BROWSER, 270 | array $curlOptions = [] 271 | ): Response { 272 | $client = new self($browser, $timeout, $curlOptions); 273 | 274 | return $client->sendDelete($url, $headers); 275 | } 276 | 277 | /** 278 | * Static version - Patch a resource at a URL 279 | * 280 | * @param string $url The URL to request 281 | * @param array|null $data Data to send with the PATCH request 282 | * @param array $headers Headers to send with the request 283 | * @param int $timeout Timeout in seconds 284 | * @param string $browser Browser to impersonate 285 | * @param array $curlOptions Custom curl options to add to the request 286 | * @return Response 287 | * @throws RequestException 288 | */ 289 | public static function patch( 290 | string $url, 291 | ?array $data = null, 292 | array $headers = [], 293 | int $timeout = self::DEFAULT_TIMEOUT, 294 | string $browser = self::DEFAULT_BROWSER, 295 | array $curlOptions = [] 296 | ): Response { 297 | $client = new self($browser, $timeout, $curlOptions); 298 | 299 | return $client->sendPatch($url, $data, $headers); 300 | } 301 | 302 | /** 303 | * Static version - Put a resource at a URL 304 | * 305 | * @param string $url The URL to request 306 | * @param array|null $data Data to send with the PUT request 307 | * @param array $headers Headers to send with the request 308 | * @param int $timeout Timeout in seconds 309 | * @param string $browser Browser to impersonate 310 | * @param array $curlOptions Custom curl options to add to the request 311 | * @return Response 312 | * @throws RequestException 313 | */ 314 | public static function put( 315 | string $url, 316 | ?array $data = null, 317 | array $headers = [], 318 | int $timeout = self::DEFAULT_TIMEOUT, 319 | string $browser = self::DEFAULT_BROWSER, 320 | array $curlOptions = [] 321 | ): Response { 322 | $client = new self($browser, $timeout, $curlOptions); 323 | 324 | return $client->sendPut($url, $data, $headers); 325 | } 326 | 327 | /** 328 | * Create temporary files for the request/response 329 | * 330 | * @return array{body: string, headers: string} 331 | */ 332 | private function createTempFiles(): array 333 | { 334 | // Create with more reliable permissions 335 | $bodyFile = tempnam(sys_get_temp_dir(), 'curl_impersonate_body'); 336 | $headerFile = tempnam(sys_get_temp_dir(), 'curl_impersonate_headers'); 337 | 338 | // Ensure files are writable 339 | if (! is_writable($bodyFile) || ! is_writable($headerFile)) { 340 | throw new RequestException("Unable to create writable temporary files"); 341 | } 342 | 343 | // Set permissions to be extra sure 344 | chmod($bodyFile, 0644); 345 | chmod($headerFile, 0644); 346 | 347 | return [ 348 | 'body' => $bodyFile, 349 | 'headers' => $headerFile, 350 | ]; 351 | } 352 | 353 | /** 354 | * Clean up temporary files 355 | * 356 | * @param array{body: string, headers: string} $files Temporary files to clean up 357 | */ 358 | private function cleanupTempFiles(array $files): void 359 | { 360 | foreach ($files as $file) { 361 | if (file_exists($file)) { 362 | unlink($file); 363 | } 364 | } 365 | } 366 | 367 | /** 368 | * Build the curl command with proper handling of JSON data 369 | */ 370 | private function buildCommand( 371 | string $method, 372 | string $url, 373 | string $outputFile, 374 | string $headerFile, 375 | array $headers = [], 376 | ?string $body = null 377 | ): string { 378 | $browserCmd = $this->browser->getExecutablePath(); 379 | 380 | // Base command with method and URL 381 | $cmd = sprintf( 382 | '%s -s -L -w "%%{http_code}" --max-time %d -o "%s" -D "%s" -X %s', 383 | escapeshellcmd($browserCmd), 384 | $this->timeout, 385 | $outputFile, 386 | $headerFile, 387 | $method 388 | ); 389 | 390 | // Add headers 391 | foreach ($headers as $name => $value) { 392 | $cmd .= sprintf(' -H %s', escapeshellarg("$name: $value")); 393 | } 394 | 395 | // Add request body if present 396 | if ($body !== null) { 397 | // Check if it's JSON data 398 | $isJson = isset($headers['Content-Type']) && 399 | strpos($headers['Content-Type'], 'application/json') !== false; 400 | 401 | if ($isJson) { 402 | // Create temporary file for the data 403 | $bodyFile = tempnam(sys_get_temp_dir(), 'curl_impersonate_body'); 404 | file_put_contents($bodyFile, $body); 405 | $cmd .= sprintf(' --data-binary @%s', escapeshellarg($bodyFile)); 406 | // Add cleanup for this file 407 | register_shutdown_function(function () use ($bodyFile) { 408 | if (file_exists($bodyFile)) { 409 | @unlink($bodyFile); 410 | } 411 | }); 412 | } else { 413 | // For form data, use direct data parameter 414 | $cmd .= sprintf(' --data %s', escapeshellarg($body)); 415 | } 416 | } 417 | 418 | // Add custom curl options 419 | foreach ($this->curlOptions as $option => $value) { 420 | if (is_bool($value)) { 421 | if ($value) { 422 | $cmd .= " --$option"; 423 | } 424 | } else { 425 | $cmd .= sprintf(' --%s %s', $option, escapeshellarg((string)$value)); 426 | } 427 | } 428 | 429 | // Add URL 430 | $cmd .= ' ' . escapeshellarg($url); 431 | 432 | return $cmd; 433 | } 434 | 435 | /** 436 | * Run the curl command with timeout protection 437 | * 438 | * @param string $command The command to execute 439 | * @return array{status_code: string, output: array} 440 | * @throws RequestException If command execution fails 441 | */ 442 | private function runCommand(string $command): array 443 | { 444 | $output = []; 445 | $returnVar = null; 446 | 447 | // Set a process timeout slightly longer than the curl timeout 448 | $processTimeout = $this->timeout + 5; 449 | 450 | // Use proc_open instead of exec for better control 451 | $descriptorspec = [ 452 | 0 => ["pipe", "r"], // stdin 453 | 1 => ["pipe", "w"], // stdout 454 | 2 => ["pipe", "w"], // stderr 455 | ]; 456 | 457 | $process = proc_open($command, $descriptorspec, $pipes); 458 | 459 | if (! is_resource($process)) { 460 | throw new RequestException("Failed to execute command: $command"); 461 | } 462 | 463 | // Set pipes to non-blocking mode 464 | stream_set_blocking($pipes[1], false); 465 | stream_set_blocking($pipes[2], false); 466 | 467 | // Close stdin 468 | fclose($pipes[0]); 469 | 470 | // Set start time 471 | $startTime = time(); 472 | $outputContent = ''; 473 | $errorContent = ''; 474 | 475 | // Read output with timeout 476 | while (true) { 477 | $status = proc_get_status($process); 478 | 479 | // Process has exited 480 | if (! $status['running']) { 481 | break; 482 | } 483 | 484 | // Check for timeout 485 | if ((time() - $startTime) > $processTimeout) { 486 | proc_terminate($process, 9); // SIGKILL 487 | proc_close($process); 488 | 489 | throw new RequestException( 490 | "Command execution timed out after $processTimeout seconds: $command", 491 | 0, 492 | null, 493 | $command, 494 | ['Timeout occurred'] 495 | ); 496 | } 497 | 498 | // Read from stdout and stderr 499 | $stdout = fread($pipes[1], 8192); 500 | $stderr = fread($pipes[2], 8192); 501 | 502 | if ($stdout) { 503 | $outputContent .= $stdout; 504 | } 505 | 506 | if ($stderr) { 507 | $errorContent .= $stderr; 508 | } 509 | 510 | // Prevent CPU overuse 511 | usleep(10000); // 10ms 512 | } 513 | 514 | // Get any remaining output 515 | while ($stdout = fread($pipes[1], 8192)) { 516 | $outputContent .= $stdout; 517 | } 518 | 519 | while ($stderr = fread($pipes[2], 8192)) { 520 | $errorContent .= $stderr; 521 | } 522 | 523 | // Close pipes 524 | fclose($pipes[1]); 525 | fclose($pipes[2]); 526 | 527 | // Get exit code 528 | $exitCode = proc_close($process); 529 | 530 | // Process output 531 | $output = array_filter(explode("\n", $outputContent)); 532 | $errorOutput = array_filter(explode("\n", $errorContent)); 533 | 534 | // Merge stderr into output for consistent handling 535 | if (! empty($errorOutput)) { 536 | $output = array_merge($output, $errorOutput); 537 | } 538 | 539 | $lastLine = end($output); 540 | 541 | if ($exitCode !== 0) { 542 | // For HEAD requests specifically, we'll be more lenient 543 | if (strpos($command, '-X HEAD') !== false && 544 | is_numeric($lastLine) && 545 | ((int)$lastLine >= 200 && (int)$lastLine < 400)) { 546 | // This is likely a successful HEAD request despite the non-zero exit code 547 | return [ 548 | 'status_code' => $lastLine, 549 | 'output' => $output, 550 | ]; 551 | } 552 | 553 | // Otherwise, throw an exception with the output for debugging 554 | $errorOutput = implode("\n", $output); 555 | 556 | throw new RequestException( 557 | "Command execution failed with code $exitCode: $errorOutput", 558 | $exitCode, 559 | null, 560 | $command, 561 | $output 562 | ); 563 | } 564 | 565 | return [ 566 | 'status_code' => is_numeric($lastLine) ? $lastLine : '0', 567 | 'output' => $output, 568 | ]; 569 | } 570 | 571 | /** 572 | * Parse response headers 573 | * 574 | * @param string $headersContent Raw headers 575 | * @return array Parsed headers 576 | */ 577 | private function parseHeaders(string $headersContent): array 578 | { 579 | $headers = []; 580 | 581 | // Split into header sections separated by empty lines and get last response 582 | $sections = preg_split('/\r?\n\r?\n/', trim($headersContent)); 583 | 584 | if (! $sections) { 585 | return []; 586 | } 587 | 588 | $lastSection = end($sections); 589 | 590 | foreach (explode("\n", $lastSection) as $line) { 591 | $line = trim($line); 592 | if (empty($line) || strpos($line, 'HTTP/') === 0) { 593 | continue; 594 | } 595 | 596 | $parts = explode(':', $line, 2); 597 | if (count($parts) === 2) { 598 | $headers[trim($parts[0])] = trim($parts[1]); 599 | } 600 | } 601 | 602 | return $headers; 603 | } 604 | 605 | /** 606 | * Normalize headers from various formats to a consistent format 607 | * 608 | * @param array $headers Headers in various formats 609 | * @return array Normalized headers 610 | */ 611 | private function normalizeHeaders(array $headers): array 612 | { 613 | $normalized = []; 614 | 615 | foreach ($headers as $key => $value) { 616 | if (is_numeric($key)) { 617 | // If it contains a colon, it's already formatted 618 | if (is_string($value) && strpos($value, ':') !== false) { 619 | list($headerName, $headerValue) = array_map('trim', explode(':', $value, 2)); 620 | $normalized[$headerName] = $headerValue; 621 | } 622 | } else { 623 | $normalized[$key] = $value; 624 | } 625 | } 626 | 627 | return $normalized; 628 | } 629 | } 630 | -------------------------------------------------------------------------------- /src/PHPImpersonateFactory.php: -------------------------------------------------------------------------------- 1 | $headers Headers to send with the request 17 | * @param int $timeout Timeout in seconds 18 | * @param string $browser Browser to impersonate 19 | * @param array $curlOptions Custom curl options to add to the request 20 | * @return Response 21 | * @throws RequestException 22 | */ 23 | public static function get( 24 | string $url, 25 | array $headers = [], 26 | int $timeout = 30, 27 | string $browser = 'chrome99_android', 28 | array $curlOptions = [] 29 | ): Response { 30 | $client = new PHPImpersonate($browser, $timeout, $curlOptions); 31 | 32 | return $client->sendGet($url, $headers); 33 | } 34 | 35 | /** 36 | * Post data to a URL and return response 37 | * 38 | * @param string $url The URL to request 39 | * @param array|null $data Data to send with the POST request 40 | * @param array $headers Headers to send with the request 41 | * @param int $timeout Timeout in seconds 42 | * @param string $browser Browser to impersonate 43 | * @param array $curlOptions Custom curl options to add to the request 44 | * @return Response 45 | * @throws RequestException 46 | */ 47 | public static function post( 48 | string $url, 49 | ?array $data = null, 50 | array $headers = [], 51 | int $timeout = 30, 52 | string $browser = 'chrome99_android', 53 | array $curlOptions = [] 54 | ): Response { 55 | $client = new PHPImpersonate($browser, $timeout, $curlOptions); 56 | 57 | return $client->sendPost($url, $data, $headers); 58 | } 59 | 60 | /** 61 | * Get headers and status code for a URL using HEAD request 62 | * 63 | * @param string $url The URL to request 64 | * @param array $headers Headers to send with the request 65 | * @param int $timeout Timeout in seconds 66 | * @param string $browser Browser to impersonate 67 | * @param array $curlOptions Custom curl options to add to the request 68 | * @return Response 69 | * @throws RequestException 70 | */ 71 | public static function head( 72 | string $url, 73 | array $headers = [], 74 | int $timeout = 30, 75 | string $browser = 'chrome99_android', 76 | array $curlOptions = [] 77 | ): Response { 78 | $client = new PHPImpersonate($browser, $timeout, $curlOptions); 79 | 80 | return $client->sendHead($url, $headers); 81 | } 82 | 83 | /** 84 | * Delete a resource at a URL 85 | * 86 | * @param string $url The URL to request 87 | * @param array $headers Headers to send with the request 88 | * @param int $timeout Timeout in seconds 89 | * @param string $browser Browser to impersonate 90 | * @param array $curlOptions Custom curl options to add to the request 91 | * @return Response 92 | * @throws RequestException 93 | */ 94 | public static function delete( 95 | string $url, 96 | array $headers = [], 97 | int $timeout = 30, 98 | string $browser = 'chrome99_android', 99 | array $curlOptions = [] 100 | ): Response { 101 | $client = new PHPImpersonate($browser, $timeout, $curlOptions); 102 | 103 | return $client->sendDelete($url, $headers); 104 | } 105 | 106 | /** 107 | * Patch a resource at a URL 108 | * 109 | * @param string $url The URL to request 110 | * @param array|null $data Data to send with the PATCH request 111 | * @param array $headers Headers to send with the request 112 | * @param int $timeout Timeout in seconds 113 | * @param string $browser Browser to impersonate 114 | * @param array $curlOptions Custom curl options to add to the request 115 | * @return Response 116 | * @throws RequestException 117 | */ 118 | public static function patch( 119 | string $url, 120 | ?array $data = null, 121 | array $headers = [], 122 | int $timeout = 30, 123 | string $browser = 'chrome99_android', 124 | array $curlOptions = [] 125 | ): Response { 126 | $client = new PHPImpersonate($browser, $timeout, $curlOptions); 127 | 128 | return $client->sendPatch($url, $data, $headers); 129 | } 130 | 131 | /** 132 | * Put a resource at a URL 133 | * 134 | * @param string $url The URL to request 135 | * @param array|null $data Data to send with the PUT request 136 | * @param array $headers Headers to send with the request 137 | * @param int $timeout Timeout in seconds 138 | * @param string $browser Browser to impersonate 139 | * @param array $curlOptions Custom curl options to add to the request 140 | * @return Response 141 | * @throws RequestException 142 | */ 143 | public static function put( 144 | string $url, 145 | ?array $data = null, 146 | array $headers = [], 147 | int $timeout = 30, 148 | string $browser = 'chrome99_android', 149 | array $curlOptions = [] 150 | ): Response { 151 | $client = new PHPImpersonate($browser, $timeout, $curlOptions); 152 | 153 | return $client->sendPut($url, $data, $headers); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | $headers Request headers 11 | * @param string|null $body Request body content 12 | */ 13 | public function __construct( 14 | private string $method, 15 | private string $url, 16 | private array $headers = [], 17 | private ?string $body = null 18 | ) { 19 | } 20 | 21 | /** 22 | * Get the request method 23 | * 24 | * @return string 25 | */ 26 | public function getMethod(): string 27 | { 28 | return $this->method; 29 | } 30 | 31 | /** 32 | * Get the request URL 33 | * 34 | * @return string 35 | */ 36 | public function getUrl(): string 37 | { 38 | return $this->url; 39 | } 40 | 41 | /** 42 | * Get the request headers 43 | * 44 | * @return array 45 | */ 46 | public function getHeaders(): array 47 | { 48 | return $this->headers; 49 | } 50 | 51 | /** 52 | * Get the request body 53 | * 54 | * @return string|null 55 | */ 56 | public function getBody(): ?string 57 | { 58 | return $this->body; 59 | } 60 | 61 | /** 62 | * Create a new request with the given headers 63 | * 64 | * @param array $headers 65 | * @return self 66 | */ 67 | public function withHeaders(array $headers): self 68 | { 69 | $clone = clone $this; 70 | $clone->headers = array_merge($this->headers, $headers); 71 | 72 | return $clone; 73 | } 74 | 75 | /** 76 | * Create a new request with the given body 77 | * 78 | * @param string|null $body 79 | * @return self 80 | */ 81 | public function withBody(?string $body): self 82 | { 83 | $clone = clone $this; 84 | $clone->body = $body; 85 | 86 | return $clone; 87 | } 88 | 89 | /** 90 | * Create a GET request 91 | * 92 | * @param string $url 93 | * @param array $headers 94 | * @return self 95 | */ 96 | public static function get(string $url, array $headers = []): self 97 | { 98 | return new self('GET', $url, $headers); 99 | } 100 | 101 | /** 102 | * Create a POST request 103 | * 104 | * @param string $url 105 | * @param array $headers 106 | * @param string|null $body 107 | * @return self 108 | */ 109 | public static function post(string $url, array $headers = [], ?string $body = null): self 110 | { 111 | return new self('POST', $url, $headers, $body); 112 | } 113 | 114 | /** 115 | * Create a HEAD request 116 | * 117 | * @param string $url 118 | * @param array $headers 119 | * @return self 120 | */ 121 | public static function head(string $url, array $headers = []): self 122 | { 123 | return new self('HEAD', $url, $headers); 124 | } 125 | 126 | /** 127 | * Create a DELETE request 128 | * 129 | * @param string $url 130 | * @param array $headers 131 | * @return self 132 | */ 133 | public static function delete(string $url, array $headers = []): self 134 | { 135 | return new self('DELETE', $url, $headers); 136 | } 137 | 138 | /** 139 | * Create a PATCH request 140 | * 141 | * @param string $url 142 | * @param array $headers 143 | * @param string|null $body 144 | * @return self 145 | */ 146 | public static function patch(string $url, array $headers = [], ?string $body = null): self 147 | { 148 | return new self('PATCH', $url, $headers, $body); 149 | } 150 | 151 | /** 152 | * Create a PUT request 153 | * 154 | * @param string $url 155 | * @param array $headers 156 | * @param string|null $body 157 | * @return self 158 | */ 159 | public static function put(string $url, array $headers = [], ?string $body = null): self 160 | { 161 | return new self('PUT', $url, $headers, $body); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | $headers Response headers 11 | */ 12 | public function __construct( 13 | private string $body, 14 | private int $statusCode, 15 | private array $headers 16 | ) { 17 | } 18 | 19 | /** 20 | * Get the response body 21 | * 22 | * @return string 23 | */ 24 | public function body(): string 25 | { 26 | return $this->body; 27 | } 28 | 29 | /** 30 | * Parse the response body as JSON 31 | * 32 | * @param bool $associative When true, returns array instead of object 33 | * @param int $depth Maximum nesting depth 34 | * @param int $flags JSON decode flags 35 | * @return mixed 36 | * @throws \JsonException If JSON decoding fails 37 | */ 38 | public function json(bool $associative = true, int $depth = 512, int $flags = 0): mixed 39 | { 40 | return json_decode($this->body, $associative, $depth, $flags | JSON_THROW_ON_ERROR); 41 | } 42 | 43 | /** 44 | * Get the response status code 45 | * 46 | * @return int 47 | */ 48 | public function status(): int 49 | { 50 | return $this->statusCode; 51 | } 52 | 53 | /** 54 | * Get all response headers 55 | * 56 | * @return array 57 | */ 58 | public function headers(): array 59 | { 60 | return $this->headers; 61 | } 62 | 63 | /** 64 | * Get a specific header value 65 | * 66 | * @param string $name Header name (case-insensitive) 67 | * @param string|null $default Default value if header not found 68 | * @return string|null 69 | */ 70 | public function header(string $name, ?string $default = null): ?string 71 | { 72 | // Case-insensitive header lookup 73 | foreach ($this->headers as $key => $value) { 74 | if (strcasecmp($key, $name) === 0) { 75 | return $value; 76 | } 77 | } 78 | 79 | return $default; 80 | } 81 | 82 | /** 83 | * Check if the response was successful (status code 200-299) 84 | * 85 | * @return bool 86 | */ 87 | public function isSuccess(): bool 88 | { 89 | return $this->statusCode >= 200 && $this->statusCode < 300; 90 | } 91 | 92 | /** 93 | * For backward compatibility with array access 94 | * 95 | * @return array{body: string, statusCode: int, headers: array} 96 | */ 97 | public function toArray(): array 98 | { 99 | return [ 100 | 'body' => $this->body, 101 | 'statusCode' => $this->statusCode, 102 | 'headers' => $this->headers, 103 | ]; 104 | } 105 | 106 | /** 107 | * Dump response details for debugging 108 | * 109 | * @return string 110 | */ 111 | public function dump(): string 112 | { 113 | $output = "HTTP Status: {$this->statusCode}\n\n"; 114 | 115 | // Headers 116 | $output .= "Headers:\n"; 117 | foreach ($this->headers as $name => $value) { 118 | $output .= "$name: $value\n"; 119 | } 120 | 121 | // Body preview 122 | $output .= "\nBody (first 500 chars):\n"; 123 | $output .= substr($this->body, 0, 500); 124 | 125 | if (strlen($this->body) > 500) { 126 | $output .= "...[truncated]"; 127 | } 128 | 129 | return $output; 130 | } 131 | 132 | /** 133 | * Debug response details to output 134 | * 135 | * @return self 136 | */ 137 | public function debug(): self 138 | { 139 | echo $this->dump(); 140 | 141 | return $this; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/utils.php: -------------------------------------------------------------------------------- 1 |