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, AUTHOR vividsnow LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.