Curl-Impersonate
view release on metacpan or search on metacpan
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, unless "default_headers"
is 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 "3xx" redirects. Default false. libcurl bounds the chain
itself, so a redirect loop ends with "Number of redirects hit
maximum amount" rather than spinning.
decode => $bool
Decompress the response body. libcurl decodes whatever it was built
with -- here gzip, deflate, br and zstd -- and "$res->{body}" is the
decoded bytes. Off by default, so the body arrives exactly as the
origin sent it.
This does not change the request on the wire. The "Accept-Encoding"
the impersonate target installs is a custom header, and libcurl lets
a custom header replace the one it would generate itself, so the
fingerprint is untouched; only the response side differs.
"$res->{headers}" still reports the origin's "Content-Encoding" and
"Content-Length", which now describe the bytes before decoding.
Anything forwarding this response onward must drop both.
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 with "CONNECT".
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 }
( run in 1.330 second using v1.01-cache-2.11-cpan-5c0b1e786e0 )