File Coverage

blib/lib/Net/ACME/HTTP.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Net::ACME::HTTP;
2              
3             =encoding utf-8
4              
5             =head1 NAME
6              
7             Net::ACME::HTTP - transport logic for C.
8              
9             =head1 SYNOPSIS
10              
11             my $resp = Net::ACME::HTTP->new()->get('https://url/to/endpoint');
12              
13             my $http_authn = Net::ACME::HTTP->new( key => $account_key );
14              
15             my $post_resp = $http_authn->post(
16             'https://url/to/endpoint',
17             { foo => 4 }, #i.e., the payload to send
18             %opts, #cf. HTTP::Tiny::request
19             );
20              
21             =head1 DESCRIPTION
22              
23             This module handles communication with an ACME server at the HTTP level.
24             It handles the wrapping of POSTs in JWSes (JSON Wed Signatures).
25              
26             Failure responses prompt exceptions. This includes cases like HTTP 409
27             from “new-reg”, which maybe isn’t an B case so much as just
28             something to accommodate.
29              
30             =cut
31              
32 7     7   211154 use strict;
  7         9  
  7         155  
33 7     7   29 use warnings;
  7         13  
  7         120  
34              
35 7     7   2500 use JSON ();
  7         29228  
  7         113  
36              
37 7     7   2337 use Net::ACME::Crypt ();
  0            
  0            
38             use Net::ACME::Error ();
39             use Net::ACME::HTTP_Tiny ();
40             use Net::ACME::HTTP::Response ();
41             use Net::ACME::Utils ();
42             use Net::ACME::X ();
43              
44             #accessed from tests
45             our $_NONCE_HEADER = 'replay-nonce';
46              
47             #Used in testing
48             our $verify_SSL = 1;
49              
50             #NB: “key” isn’t needed if we’re just doing GETs.
51             sub new {
52             my ( $class, %opts ) = @_;
53              
54             my $ua = Net::ACME::HTTP_Tiny->new( verify_SSL => $verify_SSL );
55              
56             my $self = bless {
57             _ua => $ua,
58             _acme_key => $opts{'key'},
59             }, $class;
60              
61             return bless $self, $class;
62             }
63              
64             #GETs submit no data and thus are not signed.
65             sub get {
66             my ( $self, $url ) = @_;
67              
68             return $self->_request_and_set_last_nonce( 'get', $url );
69             }
70              
71             #POSTs are signed.
72             sub post {
73             my ( $self, $url, $data, $opts_hr ) = @_;
74              
75             die "Constructor needed “key” to do POST! ($url)" if !$self->{'_acme_key'};
76              
77             my $jws = $self->_create_jws( JSON->new()->allow_nonref()->encode($data) );
78              
79             return $self->_request_and_set_last_nonce(
80             'post',
81             $url,
82             { content => $jws },
83             $opts_hr || (),
84             );
85             }
86              
87             sub request {
88             }
89              
90             #----------------------------------------------------------------------
91              
92             sub _ua_request {
93             my ( $self, $type, @args ) = @_;
94              
95             return $self->{'_ua'}->request( $type, @args );
96             }
97              
98             #overridden in tests
99             sub _request {
100             my ( $self, $type, @args ) = @_;
101              
102             my $resp;
103              
104             #cf. eval_bug.readme
105             my $eval_err = $@;
106              
107             eval { $resp = $self->_ua_request( $type, @args ); };
108              
109             if ($@) {
110             my $exc = $@;
111              
112             if ( eval { $exc->isa('Net::ACME::X::HTTP::Protocol') } ) {
113             my $_nonce_header_lc = $_NONCE_HEADER;
114             $_nonce_header_lc =~ tr;
115              
116             my $nonce = $exc->get('headers')->{$_nonce_header_lc};
117             $self->{'_last_nonce'} = $nonce if $nonce;
118              
119             #If the exception is able to be made into a Net::ACME::Error,
120             #then do so to get a nicer error message.
121             my $acme_error = eval {
122             Net::ACME::Error->new(
123             %{ JSON::decode_json( $exc->get('content') ) },
124             );
125             };
126              
127             my $detail;
128             if ($acme_error) {
129             $detail = $acme_error->detail();
130             my $desc = $acme_error->description();
131             if ($desc) {
132             $detail = sprintf "%s (%s)", $detail, $desc;
133             }
134             }
135             else {
136             $detail = $exc->get('content');
137             }
138              
139             die Net::ACME::X::create(
140             'Protocol',
141             {
142             ( map { $_ => $exc->get($_) } qw( url status reason headers ) ),
143             type => $acme_error ? $acme_error->type() : '(unknown type)',
144             detail => $detail,
145             }
146             );
147             }
148              
149             $@ = $exc;
150             die;
151             }
152              
153             $@ = $eval_err;
154              
155             return Net::ACME::HTTP::Response->new($resp);
156             }
157              
158             sub _request_and_set_last_nonce {
159             my ( $self, $type, @args ) = @_;
160              
161             my $resp = $self->_request( $type, @args );
162              
163             #NB: ACME’s replay protection works thus:
164             # - each server response includes a nonce
165             # - each request must include ONE of the nonces that have been sent
166             # - once used, a nonce can’t be reused
167             #
168             #This is subtly different from what was originally in mind (i.e., that
169             #each request must use the most recently sent nonce). It implies that GETs
170             #do not need to send nonces, though each GET will *receive* a nonce that
171             #may be used.
172             if ( my $nonce = $resp->header($_NONCE_HEADER) ) {
173             $self->{'_last_nonce'} = $nonce;
174             }
175              
176             return $resp;
177             }
178              
179             sub _create_jws {
180             my ( $self, $msg ) = @_;
181              
182             die "Need a nonce before JWS can be created!" if !$self->{'_last_nonce'};
183              
184             return Net::ACME::Crypt::create_rs256_jwt(
185             key => $self->{'_acme_key'},
186             extra_headers => {
187             nonce => $self->{'_last_nonce'},
188             jwk => Net::ACME::Utils::get_jwk_data( $self->{'_acme_key'} ),
189             },
190             payload => $msg,
191             );
192             }
193              
194             1;