At
view release on metacpan or search on metacpan
lib/At/UserAgent.pm view on Meta::CPAN
method dpop_nonce ( $new_val = undef ) {
$dpop_nonce = $new_val if defined $new_val;
return $dpop_nonce;
}
method auth ( $new_val = undef ) {
$auth = $new_val if defined $new_val;
return $auth;
}
method set_tokens ( $access, $refresh, $type, $key ) {
$accessJwt = $access;
$refreshJwt = $refresh;
$token_type = $type // 'Bearer';
$dpop_key = $key;
if ( defined $accessJwt ) {
$self->_set_auth_header( $token_type . ' ' . $accessJwt );
}
else {
$self->_set_auth_header(undef);
}
}
method _generate_dpop_proof( $url, $method, $skip_ath = 0 ) {
return unless $dpop_key;
my $jwk_json = $dpop_key->export_key_jwk('public');
my $jwk = JSON::PP::decode_json($jwk_json);
my $now = time;
my $htu = URI->new($url);
$htu->query(undef);
$htu->fragment(undef);
my $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~';
my $payload
= { jti => Crypt::PRNG::random_string_from( $chars, 32 ), htm => $method, htu => $htu->as_string, iat => $now, exp => $now + 60, };
$payload->{nonce} = $dpop_nonce if defined $dpop_nonce;
if ( $accessJwt && !$skip_ath ) {
$payload->{ath} = MIME::Base64::encode_base64url( Digest::SHA::sha256($accessJwt) );
$payload->{ath} =~ s/=+$//;
}
return Crypt::JWT::encode_jwt( payload => $payload, key => $dpop_key, alg => 'ES256', extra_headers => { typ => 'dpop+jwt', jwk => $jwk } );
}
method _set_auth_header ($token) { die "Abstract" }
method get ( $url, $req = undef ) { die "Abstract" }
method post ( $url, $req = undef ) { die "Abstract" }
method websocket ( $url, $cb ) { die "Abstract" }
}
class #
At::UserAgent::Tiny : isa(At::UserAgent) {
use HTTP::Tiny;
field $agent : param
= HTTP::Tiny->new( agent => 'At.pm/Tiny', default_headers => { 'Content-Type' => 'application/json', Accept => 'application/json' } );
method get( $url, $req = {} ) {
$req //= {};
$req->{headers}{DPoP} = $self->_generate_dpop_proof( $url, 'GET', $req->{skip_ath} ) if $self->token_type eq 'DPoP';
my $res
= $agent->get( $url . ( defined $req->{content} && keys %{ $req->{content} } ? '?' . $agent->www_form_urlencode( $req->{content} ) : '' ),
{ defined $req->{headers} ? ( headers => $req->{headers} ) : () } );
$res->{content} = JSON::PP::decode_json( $res->{content} ) if $res->{content} && ( $res->{headers}{'content-type'} // '' ) =~ m[json];
unless ( $res->{success} ) {
my $msg = $res->{reason} // 'Unknown error';
if ( ref $res->{content} eq 'HASH' ) {
my $json = $res->{content};
my $details = $json->{error} // '';
if ( $json->{message} && $json->{message} ne $details ) {
$details .= ( $details ? ': ' : '' ) . $json->{message};
}
$msg .= ": " . $details if $details;
$msg .= " - " . $json->{error_description} if $json->{error_description};
}
elsif ( $res->{content} ) {
$msg .= " (" . $res->{content} . ")";
}
$res->{content} = At::Error->new( message => $msg, fatal => 1 );
}
wantarray ? ( $res->{content}, $res->{headers} ) : $res->{content};
}
method post( $url, $req = {} ) {
$req //= {};
$req->{headers}{DPoP} = $self->_generate_dpop_proof( $url, 'POST', $req->{skip_ath} ) if $self->token_type eq 'DPoP';
my $content;
if ( defined $req->{content} ) {
if ( $req->{encoding} && $req->{encoding} eq 'form' ) {
$content = $agent->www_form_urlencode( $req->{content} );
$req->{headers}{'Content-Type'} = 'application/x-www-form-urlencoded';
}
elsif ( ref $req->{content} ) {
$content = JSON::PP::encode_json( $req->{content} );
$req->{headers}{'Content-Type'} = 'application/json';
}
else {
$content = $req->{content};
}
}
my $res = $agent->post( $url,
{ defined $req->{headers} ? ( headers => $req->{headers} ) : (), defined $content ? ( content => $content ) : () } );
$res->{content} = JSON::PP::decode_json( $res->{content} ) if $res->{content} && ( $res->{headers}{'content-type'} // '' ) =~ m[json];
unless ( $res->{success} ) {
my $msg = $res->{reason} // 'Unknown error';
if ( ref $res->{content} eq 'HASH' ) {
my $json = $res->{content};
my $details = $json->{error} // '';
if ( $json->{message} && $json->{message} ne $details ) {
$details .= ( $details ? ': ' : '' ) . $json->{message};
}
$msg .= ": " . $details if $details;
$msg .= " - " . $json->{error_description} if $json->{error_description};
}
elsif ( $res->{content} ) {
$msg .= " (" . $res->{content} . ")";
}
$res->{content} = At::Error->new( message => $msg, fatal => 1 );
}
wantarray ? ( $res->{content}, $res->{headers} ) : $res->{content};
}
method websocket ( $url, $cb ) {
die "At::UserAgent::Tiny does not support WebSockets. Please install Mojo::UserAgent.";
}
method _set_auth_header($token) {
$self->auth($token);
$agent->{default_headers}{Authorization} = $token;
}
} class #
At::UserAgent::Mojo : isa(At::UserAgent) {
field $agent : param = do { require Mojo::UserAgent; Mojo::UserAgent->new };
method get( $url, $req = {} ) {
$req //= {};
my $headers = { %{ $req->{headers} // {} } };
if ( $self->at_protocol_proxy ) {
$headers->{'atproto-proxy'} = $self->at_protocol_proxy;
}
$headers->{Authorization} = $self->auth if defined $self->auth;
$headers->{DPoP} = $self->_generate_dpop_proof( $url, 'GET', $req->{skip_ath} ) if $self->token_type eq 'DPoP';
if ( $ENV{DEBUG} ) {
say "[DEBUG] [At] GET $url";
say "[DEBUG] [At] Headers: " . JSON::PP::encode_json($headers);
}
my $tx = $agent->get( $url, $headers, defined $req->{content} ? ( form => $req->{content} ) : () );
my $res = $tx->result;
if ( my $nonce = $res->headers->header('DPoP-Nonce') ) { $self->dpop_nonce($nonce); }
if ( $ENV{DEBUG} ) {
say "[DEBUG] [At] Response Code: " . $res->code;
say "[DEBUG] [At] Response Headers: " . JSON::PP::encode_json( $res->headers->to_hash );
}
if ( $res->code == 401 || $res->code == 400 ) {
my $body = $res->body // '';
if ( $body =~ /use_dpop_nonce/i ) {
say "[DEBUG] [At] Retrying with fresh DPoP nonce..." if $ENV{DEBUG};
$headers->{DPoP} = $self->_generate_dpop_proof( $url, 'GET', $req->{skip_ath} ) if $self->token_type eq 'DPoP';
if ( $ENV{DEBUG} ) {
say "[DEBUG] [At] GET (Retry) $url";
say "[DEBUG] [At] Headers (Retry): " . JSON::PP::encode_json($headers);
}
$tx = $agent->get( $url, $headers, defined $req->{content} ? ( form => $req->{content} ) : () );
( run in 0.811 second using v1.01-cache-2.11-cpan-524268b4103 )