NAME
Curl::Impersonate - HTTP client that impersonates a browser's TLS/HTTP2 fingerprint
SYNOPSIS
use Curl::Impersonate;
# synchronous
my $c = Curl::Impersonate->new(impersonate => 'chrome131', timeout => 20);
my $res = $c->get('https://example.com/');
# $res = { status => 200, headers => { 'content-type' => '...' }, body => '...' }
my $post = $c->request(
method => 'POST',
url => 'https://example.com/api',
headers => { 'content-type' => 'application/json' },
body => '{"hello":"world"}',
);
# which browsers can I be?
my @targets = Curl::Impersonate->targets;
# asynchronous (concurrent upstreams)
my $m = Curl::Impersonate->multi;
for my $url (@urls) {
my $h = Curl::Impersonate->new(impersonate => 'chrome131');
$m->add($h, { url => $url }, sub {
my ($res, $err) = @_;
$err ? warn($err) : print $res->{status}, "\n";
});
}
$m->perform_blocking;
DESCRIPTION
Wraps libcurl-impersonate (a patched libcurl built against BoringSSL, via Alien::curlimpersonate) so a request carries a chosen real browser's TLS (JA3/JA4) and HTTP/2 (Akamai) connection fingerprint. Origin certificate verification stays on by default; impersonation changes the handshake shape, not whether the peer is verified.
This is an HTTP client, not a full browser. It reproduces the connection fingerprint (TLS + HTTP/2); it does not run JavaScript, and HTTP/3 and WebSockets are out of scope in this release.
REQUIREMENTS
Requires Alien::curlimpersonate 0.02 or newer, which builds libcurl-impersonate (a patched curl plus BoringSSL) from source at install time. That build needs a C/C++ toolchain, cmake, ninja, go and patch -- see that module for the details. No system libcurl-impersonate is used.
METHODS
new
my $c = Curl::Impersonate->new(%opt);
Creates a client (one reusable connection handle). Options:
- impersonate => $target
-
A browser profile name (see "targets"), e.g.
'chrome131'. Applies that browser's TLS/HTTP2 fingerprint and, unlessdefault_headersis false, its default header set. An unknown target croaks. - default_headers => $bool
-
Whether to also install the target's default request headers. Default true.
- timeout => $seconds
-
Whole-request timeout. There is no default: libcurl waits indefinitely, so a blackholed address or a server that accepts and never answers will hang the caller. Set one for anything talking to the open internet.
- verify => $bool
-
TLS peer/host verification. Default true. Set false only for testing against self-signed endpoints.
- follow_redirects => $bool
-
Follow
3xxredirects. Default false. libcurl bounds the chain itself, so a redirect loop ends withNumber of redirects hit maximum amountrather than spinning. - proxy => $url
-
Route requests through a proxy, e.g.
'http://127.0.0.1:8080'or'socks5h://host:1080'. Credentials go in the URL. Note that a proxy which terminates TLS presents its own fingerprint, not the impersonated one; to keep the fingerprint intact the proxy must tunnel withCONNECT.
get
my $res = $c->get($url);
Convenience for a GET request.
request
my $res = $c->request(
method => 'GET', # default GET
url => $url, # required
headers => \%headers, # optional; values are strings
body => $bytes, # optional request body
);
A header name or value containing CR, LF or NUL croaks: libcurl would pass such a line through verbatim and the origin would read it as extra headers. An undef value removes a header the impersonation profile would otherwise send.
Performs the request and returns a hashref. On success:
{ status => $int, headers => \%response_headers, body => $bytes,
url => $effective_url }
url is where the request actually ended up, which differs from the one asked for when follow_redirects sent it elsewhere.
Response header names are lower-cased; a header that appears more than once (e.g. set-cookie) is kept as an arrayref of its values. On a transport-level failure (DNS, TLS, timeout) it returns instead:
{ error => $string, code => $curl_errno }
targets
my @names = Curl::Impersonate->targets;
A sorted list of impersonation profiles verified against the built library. The underlying library may accept additional names; any string it recognises works when passed to new.
ASYNCHRONOUS INTERFACE
The methods below belong to Curl::Impersonate::Multi, which has no constructor of its own -- it is documented here because multi is the only way to get one.
multi
my $m = Curl::Impersonate->multi;
Returns a Curl::Impersonate::Multi, a curl_multi-backed handle for running several requests concurrently.
add
$m->add($handle, \%request, sub { my ($res, $err) = @_; ... });
Queues $request (same keys as "request") on $handle (a Curl::Impersonate object). The callback fires exactly once on completion with either ($res, undef) or (undef, $error_string). $handle is kept alive until then; use one handle per in-flight request.
If the callback closes over $m itself, that forms a reference cycle ($m -> queued request -> callback -> $m) which is broken only when the request completes or is "remove"d. Dropping $m while such a request is still in flight leaks the cycle -- and the underlying curl handles -- until process exit. Drive every request to completion, remove it, or Scalar::Util::weaken the captured $m.
perform_blocking
$m->perform_blocking;
Runs an internal poll loop until every queued request has completed and its callback has fired. Convenient for scripts and tests.
It cannot drive paused transfers: if an "add_streaming" on_body returns a true value (pause), perform_blocking has no way to resume it and will spin. Use the "Event-loop integration" surface for streaming/backpressure.
add_streaming
$m->add_streaming($handle, \%request, {
on_headers => sub { my ($status, $headers) = @_; ... },
on_body => sub { my ($chunk) = @_; ...; return $pause },
on_done => sub { my ($err) = @_; ... },
});
Like add, but delivers the response incrementally instead of buffering it. on_headers fires once when the upstream status and headers are known; on_body fires per body chunk. Returning a true value from on_body pauses the upstream transfer (CURLPAUSE_RECV) -- use this to apply backpressure when your downstream consumer is full; return the value 2 to abort the transfer (its on_done then fires with an error). on_headers also fires for a bodyless response. on_done fires once at the end with an error string or undef. Do not call "resume"/"remove" from inside these callbacks. Resume a paused transfer with "resume".
Pausing and the chunk contract: libcurl treats a pause as "this chunk was not consumed" and re-delivers it to on_body when you "resume". So decide whether to pause before consuming $chunk: if you return a pause value you must NOT have already consumed $chunk -- take it on the re-delivery instead. Consuming a chunk and then returning a pause value on the same call delivers it twice. The correct idiom is if ($full) { ...arrange resume...; return 1 } consume($chunk); return 0 (this is exactly what Proxy::Impersonate does for its HIWAT backpressure).
resume
$m->resume($handle);
Unpause a transfer paused by an on_body that returned true, and nudge the loop so delivery continues.
remove
$m->remove($handle);
Cancel an in-flight request and free its state without firing on_done -- for tearing down a request whose consumer has gone away.
Event-loop integration
For an external event loop (e.g. EV), drive the handle through the curl_multi socket-action surface instead of perform_blocking:
- set_socket_callback(sub { my ($fd, $what) = @_; ... })
-
Registered with curl.
$whatis curl'sCURL_POLL_*:1=want-read,2=want-write,3=both,4=stop watching$fd. Arm/disarm an I/O watcher on$fdaccordingly. - set_timer_callback(sub { my ($timeout_ms) = @_; ... })
-
Registered with curl. Arm a one-shot timer for
$timeout_ms(or disarm on-1). - socket_action($fd, $ev)
-
Call when a watched socket is ready:
$evbit1=readable, bit2=writable. Call with$fd = -1(and$ev = 0) when the timer fires. Completed requests' callbacks are dispatched from within this call. - timeout_ms
-
The current recommended timeout in milliseconds (
-1for none).
SEE ALSO
Alien::curlimpersonate, https://github.com/lexiforest/curl-impersonate
AUTHOR
vividsnow
LICENSE
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.