HTTP client for PHP with browser TLS/H2 fingerprinting. Built in Rust on top of wreq (BoringSSL).
Lets you make HTTP requests that look like they came from a real browser at the TLS/JA3/H2 level — useful when dealing with sites that fingerprint clients.
- Fully statically linked — only
glibcandlibgcc_s.so.1required at runtime - BoringSSL baked in — no OpenSSL dependency
- 100+ browser emulation profiles: Chrome, Firefox, Safari, Edge, Opera, OkHttp
- Every
.sois bound to a specific PHP API version at compile time (PHP 8.4 build will not load in PHP 8.3)
Download the pre-built .so for your PHP version from the Releases page, then load it:
# Check your PHP version
php --version
# Verify it loaded
php -d extension=/path/to/librnet.so -r "var_dump(extension_loaded('php-rnet'));"
# bool(true)Add to php.ini permanently:
extension=/absolute/path/to/librnet.soOr load per-script:
php -d extension=/absolute/path/to/librnet.so your_script.phpThe simplest way to use php-rnet — no client setup needed:
// GET request
$resp = RNet\get('https://httpbin.org/get');
echo $resp->status(); // 200
echo $resp->text(); // response body as string
// POST with JSON body
$resp = RNet\post('https://httpbin.org/post', [
'json' => ['name' => 'Alice', 'role' => 'admin'],
]);
$data = $resp->json(); // decoded as PHP array
echo $data['json']['name']; // Alice
// POST with form data
$resp = RNet\post('https://httpbin.org/post', [
'form' => ['username' => 'alice', 'password' => 'secret'],
]);
// Other methods
$resp = RNet\put('https://httpbin.org/put');
$resp = RNet\patch('https://httpbin.org/patch');
$resp = RNet\delete('https://httpbin.org/delete');
$resp = RNet\head('https://httpbin.org/get');
// Custom HTTP method
$resp = RNet\request('OPTIONS', 'https://httpbin.org/get');Create a client once and reuse it across requests. Shares a connection pool for better performance.
Builder methods return
voidand cannot be chained. Call them separately then callbuild().
$b = new RNet\ClientBuilder();
$b->timeout(30.0);
$client = $b->build();
$resp = $client->get('https://httpbin.org/get');
echo $resp->status(); // 200All request methods accept an optional array as the second argument:
$resp = $client->post('https://httpbin.org/post', [
// Custom headers
'headers' => [
'X-Api-Key' => 'secret',
'Accept-Language' => 'en-US',
],
// Query string → ?page=2&sort=asc
'query' => [
'page' => '2',
'sort' => 'asc',
],
// JSON body (sets Content-Type: application/json automatically)
'json' => ['name' => 'Alice', 'active' => true],
// Form body (sets Content-Type: application/x-www-form-urlencoded)
'form' => ['field' => 'value'],
// Raw string body
'body' => 'raw payload here',
// Per-request timeout override (seconds)
'timeout' => 10.0,
]);Only one of json, form, or body should be set per request. The first one found wins.
$resp = RNet\get('https://httpbin.org/get');
// Status
$resp->status(); // int — e.g. 200, 404
$resp->ok(); // bool — true if 2xx
$resp->version(); // string — e.g. "HTTP/2.0"
$resp->url(); // string — final URL after redirects
// Body (can be called multiple times — cached after first read)
$resp->text(); // string — body decoded as UTF-8
$resp->json(); // mixed — body decoded as PHP array/scalar
$resp->bytes(); // array — raw bytes
// Headers
$resp->headers(); // array — all headers as key→value
$resp->header('content-type'); // ?string — single header, case-insensitive
$resp->header('Content-Type'); // same result
// Cookies
$cookies = $resp->cookies(); // RNet\Cookie[]
foreach ($cookies as $c) {
echo $c->getName(); // string
echo $c->getValue(); // string
echo $c->getDomain() ?? ''; // ?string
echo $c->getPath() ?? ''; // ?string
var_dump($c->isHttpOnly()); // bool
var_dump($c->isSecure()); // bool
echo $c; // "name=value"
}
// Server address
$resp->remoteAddr(); // ?string — e.g. "93.184.216.34:443"
// Throw on 4xx / 5xx
$resp->raiseForStatus(); // throws RNet\StatusException if status >= 400Make your request look like it came from a real browser at the TLS/JA3/HTTP2 fingerprint level:
$b = new RNet\ClientBuilder();
$b->impersonate(RNet\Emulation::CHROME_136);
$client = $b->build();
$resp = $client->get('https://tls.browserleaks.com/json');
echo $resp->text();Available profiles:
| Family | Constants |
|---|---|
| Chrome | CHROME_100 through CHROME_138 |
| Edge | EDGE_101, EDGE_122, EDGE_127, EDGE_131, EDGE_134–EDGE_137 |
| Firefox | FIREFOX_109, FIREFOX_117, FIREFOX_128, FIREFOX_133, FIREFOX_135, FIREFOX_136, FIREFOX_139, FIREFOX_PRIVATE_135, FIREFOX_ANDROID_135 |
| Safari | SAFARI_15_3 through SAFARI_26, SAFARI_IPAD_18, SAFARI_IOS_16_5 through SAFARI_IOS_26 |
| Opera | OPERA_116–OPERA_119 |
| OkHttp | OK_HTTP_3_9, OK_HTTP_3_11, OK_HTTP_3_13, OK_HTTP_3_14, OK_HTTP_4_9, OK_HTTP_4_10, OK_HTTP_4_12, OK_HTTP_5 |
Skip certificate verification (useful for internal services or development):
$b = new RNet\ClientBuilder();
$b->verify(false); // disables both cert validation and hostname check
$client = $b->build();Route traffic through HTTP, HTTPS, or SOCKS5 proxies:
// Route all traffic (HTTP + HTTPS) through a SOCKS5 proxy
$proxy = RNet\Proxy::all('socks5://127.0.0.1:1080');
// HTTP traffic only
$proxy = RNet\Proxy::http('http://127.0.0.1:8080');
// HTTPS traffic only
$proxy = RNet\Proxy::https('https://127.0.0.1:8080');
// Proxy with authentication
$proxy = RNet\Proxy::all('socks5://user:pass@127.0.0.1:1080');
$b = new RNet\ClientBuilder();
$b->proxy($proxy);
$client = $b->build();
$resp = $client->get('https://httpbin.org/ip');
echo $resp->json()['origin']; // your proxy's IPEnable a per-client cookie jar so cookies persist across requests (like a real browser session):
$b = new RNet\ClientBuilder();
$b->cookieStore(true);
$client = $b->build();
// Login — server sets a session cookie
$client->post('https://example.com/login', [
'form' => ['username' => 'alice', 'password' => 'secret'],
]);
// Subsequent requests automatically send the session cookie
$resp = $client->get('https://example.com/dashboard');$b = new RNet\ClientBuilder();
$b->timeout(30.0); // total request timeout in seconds (0 = no limit)
$b->connectTimeout(5.0); // max time to establish the connection
$b->maxRedirects(5); // follow up to 5 redirects (0 = disable redirects)
$client = $b->build();Set default headers sent on every request from this client:
$b = new RNet\ClientBuilder();
$b->defaultHeader('Accept-Language', 'en-US,en;q=0.9');
$b->defaultHeader('X-Api-Key', 'my-secret');
$b->userAgent('MyBot/1.0');
$client = $b->build();Per-request headers override client defaults:
$resp = $client->get('https://httpbin.org/headers', [
'headers' => ['X-Api-Key' => 'override-for-this-request'],
]);Disable HTTP/2 and force HTTP/1.1:
$b = new RNet\ClientBuilder();
$b->http1Only(true);
$client = $b->build();All exceptions extend \Exception and can be caught individually or together:
try {
$resp = RNet\get('https://example.com');
$resp->raiseForStatus();
$data = $resp->json();
} catch (RNet\TimeoutException $e) {
// Request exceeded the timeout
error_log('Timed out: ' . $e->getMessage());
} catch (RNet\ConnectionException $e) {
// Could not connect (DNS failure, refused, etc.)
error_log('Connection failed: ' . $e->getMessage());
} catch (RNet\TlsException $e) {
// TLS handshake failed
error_log('TLS error: ' . $e->getMessage());
} catch (RNet\StatusException $e) {
// Server returned 4xx or 5xx
error_log('HTTP error: ' . $e->getMessage());
} catch (RNet\RedirectException $e) {
// Too many redirects or redirect loop
error_log('Redirect error: ' . $e->getMessage());
} catch (RNet\BodyException $e) {
// Error reading the response body
error_log('Body error: ' . $e->getMessage());
} catch (RNet\RequestException $e) {
// Any other request error
error_log('Request error: ' . $e->getMessage());
}Exception class hierarchy:
\Exception
├── RNet\RequestException — generic request failure
├── RNet\ConnectionException — could not connect
├── RNet\TlsException — TLS handshake failure
├── RNet\TimeoutException — request timed out
├── RNet\StatusException — 4xx / 5xx response
├── RNet\BodyException — error reading body
├── RNet\DecodingException — response decoding failure
├── RNet\RedirectException — too many / bad redirects
└── RNet\WebSocketException — WebSocket error
- Linux x86_64
- PHP 8.1 or newer (with development headers)
- Rust 1.85 or newer
- cmake 3.14 or newer (to compile BoringSSL)
- libclang 18 (used at build time only by bindgen — not needed at runtime)
- A C compiler: gcc or clang
Note: libclang is only needed during
cargo build. The compiled.sofile has no dependency on it.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/envVerify:
rustc --version # should be 1.85 or newer
cargo --versionDebian / Ubuntu:
sudo apt update
sudo apt install build-essential cmake pkg-config libclang-18-devIf libclang-18-dev is not available in your repo (e.g. Ubuntu 22.04), add the LLVM apt repository first:
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" | sudo tee /etc/apt/sources.list.d/llvm-18.list
sudo apt update
sudo apt install libclang-18-devReplace jammy with your Ubuntu codename (focal, noble, etc.) if needed.
Fedora / RHEL:
sudo dnf install gcc cmake clang-develArch Linux:
sudo pacman -S base-devel cmake clangDebian / Ubuntu:
# PHP 8.4
sudo apt install php8.4-dev
# or if you only have a generic package available
sudo apt install php-devFedora / RHEL:
sudo dnf install php-develArch Linux:
sudo pacman -S phpVerify that php-config works and points to your intended PHP version:
php-config --versionIf you have multiple PHP versions installed and php-config points to the wrong one, set it explicitly:
export PHP_CONFIG=/usr/bin/php-config8.4ext-php-rs uses bindgen to parse PHP headers at build time. You need to tell it where libclang is:
# Adjust the path to match your system
export LIBCLANG_PATH=/usr/lib/llvm-18/lib
# Only needed if libclang is not in a standard location
export LD_LIBRARY_PATH="$LIBCLANG_PATH:$LD_LIBRARY_PATH"Check where libclang was installed:
find /usr -name "libclang.so*" 2>/dev/nullIf cmake is not in your PATH (e.g. installed to a custom location), add it:
export PATH="/path/to/cmake/bin:$PATH"git clone https://github.com/takielias/php-rnet.git
cd php-rnet
cargo build --releaseThe first build will take a few minutes because it compiles BoringSSL from source. Subsequent builds are much faster.
Output:
target/release/librnet.so
Permanently (add to php.ini):
php --ini # find your php.iniextension=/absolute/path/to/librnet.soPer script:
php -d extension=/absolute/path/to/librnet.so your_script.phpVerify:
php -d extension=/absolute/path/to/librnet.so -r "var_dump(extension_loaded('php-rnet'));"
# bool(true)error: could not find tool "cc"
sudo apt install gcc
export PATH="/usr/bin:$PATH"could not find libclang
find / -name "libclang.so*" 2>/dev/null
export LIBCLANG_PATH=/usr/lib/llvm-18/libError: Could not find PHP executable
which php
# if missing:
sudo apt install php-cliphp-config: not found
sudo apt install php-dev
# or set it manually:
export PHP_CONFIG=/usr/bin/php-config8.4cmake version too old
BoringSSL requires cmake 3.14+:
cmake --version
# install newer via cmake.org or mise:
mise install cmake@latest
export PATH="$HOME/.local/share/mise/installs/cmake/latest/bin:$PATH"MIT