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, 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.

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 }

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. $what is curl's CURL_POLL_*: 1=want-read, 2=want-write, 3=both, 4=stop watching $fd. Arm/disarm an I/O watcher on $fd accordingly.

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: $ev bit 1=readable, bit 2=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 (-1 for 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.