File Coverage

blib/lib/Net/ACME2/HTTP.pm
Criterion Covered Total %
statement 73 93 78.4
branch 9 24 37.5
condition 4 9 44.4
subroutine 19 20 95.0
pod 0 6 0.0
total 105 152 69.0


line stmt bran cond sub pod time code
1             package Net::ACME2::HTTP;
2              
3             =encoding utf-8
4              
5             =head1 NAME
6              
7             Net::ACME2::HTTP - transport logic for C.
8              
9             =head1 DESCRIPTION
10              
11             This module handles communication with an ACME server at the HTTP level.
12             It wraps POSTs in JWSes (JSON Web Signatures) as needed.
13              
14             There should be no reason to interact with this class in production.
15              
16             =cut
17              
18 1     1   6 use strict;
  1         1  
  1         23  
19 1     1   4 use warnings;
  1         2  
  1         17  
20              
21 1     1   14 use JSON ();
  1         1  
  1         11  
22              
23 1     1   371 use Net::ACME2::Error ();
  1         2  
  1         16  
24 1     1   5 use Net::ACME2::HTTP_Tiny ();
  1         1  
  1         11  
25 1     1   385 use Net::ACME2::HTTP::Response ();
  1         2  
  1         15  
26 1     1   4 use Net::ACME2::X ();
  1         2  
  1         14  
27              
28 1     1   4 use constant _CONTENT_TYPE => 'application/jose+json';
  1         1  
  1         887  
29              
30             #accessed from tests
31             our $_NONCE_HEADER = 'replay-nonce';
32              
33             #Used in testing
34             our $verify_SSL = 1;
35              
36             #NB: “key” isn’t needed if we’re just doing GETs.
37             sub new {
38 1     1 0 215368 my ( $class, %opts ) = @_;
39              
40 1 50       5 die Net::ACME2::X->create('Generic', 'need “key”!') if !$opts{'key'};
41              
42 1         10 my $ua = Net::ACME2::HTTP_Tiny->new( verify_SSL => $verify_SSL );
43              
44             my $self = bless {
45             _ua => $ua,
46             _acme_key => $opts{'key'},
47             _key_id => $opts{'key_id'},
48 1         6 _jws_format => $opts{'jws_format'},
49             }, $class;
50              
51 1         5 return bless $self, $class;
52             }
53              
54             sub set_key_id {
55 2     2 0 6 my ($self, $key_id) = @_;
56              
57 2         5 $self->{'_key_id'} = $key_id;
58              
59 2         4 return $self;
60             }
61              
62             sub set_new_nonce_url {
63 5     5 0 10 my ($self, $url) = @_;
64              
65 5         7 $self->{'_nonce_url'} = $url;
66              
67 5         9 return $self;
68             }
69              
70             #GETs submit no data and thus are not signed.
71             sub get {
72 1     1 0 3 my ( $self, $url ) = @_;
73              
74 1         6 return $self->_request( 'GET', $url );
75             }
76              
77             # ACME spec 6.2: for all requests not signed using an existing account,
78             # e.g., newAccount
79             sub post_full_jwt {
80 2     2 0 5 my $self = shift;
81              
82 2         9 return $self->_post( 'create_full_jws', @_ );
83             }
84              
85             # ACME spec 6.2: for all requests signed using an existing account
86             sub post_key_id {
87 0     0 0 0 my $self = shift;
88              
89 0         0 return $self->_post(
90             'create_key_id_jws',
91             @_,
92             );
93             }
94              
95             #----------------------------------------------------------------------
96              
97             #POSTs are signed.
98             sub _post {
99 2     2   6 my ( $self, $jwt_method, $url, $data, $opts_hr ) = @_;
100              
101             # Shouldn’t be needed because the constructor requires “key”,
102             # but just in case.
103 2 50       6 die "Constructor needed “key” to do POST! ($url)" if !$self->{'_acme_key'};
104              
105 2         9 my $jws = $self->_create_jwt( $jwt_method, $url, $data );
106              
107 2         18 local $opts_hr->{'headers'}{'Content-Type'} = 'application/jose+json';
108              
109 2   33     34 return $self->_request_and_set_last_nonce(
110             'POST',
111             $url,
112             {
113             content => $jws,
114             headers => {
115             'content-type' => _CONTENT_TYPE,
116             },
117             },
118             $opts_hr || (),
119             );
120             }
121              
122             sub _ua_request {
123 4     4   13 my ( $self, $type, @args ) = @_;
124              
125 4         46 return $self->{'_ua'}->request( $type, @args );
126             }
127              
128             #overridden in tests
129             sub _request {
130 4     4   14 my ( $self, $type, @args ) = @_;
131              
132 4         8 my $resp;
133              
134             #cf. eval_bug.readme
135 4         9 my $eval_err = $@;
136              
137 4         10 eval { $resp = $self->_ua_request( $type, @args ); };
  4         18  
138              
139             # Check ref() first to avoid potentially running overload.pm’s
140             # stringification.
141 4 50 33     28 if (ref($@) || $@) {
142 0         0 my $exc = $@;
143              
144 0 0       0 if ( eval { $exc->isa('Net::ACME2::X::HTTP::Protocol') } ) {
  0         0  
145 0         0 my $_nonce_header_lc = $_NONCE_HEADER;
146 0         0 $_nonce_header_lc =~ tr;
147              
148 0         0 my $nonce = $exc->get('headers')->{$_nonce_header_lc};
149 0 0       0 $self->{'_last_nonce'} = $nonce if $nonce;
150              
151             #If the exception is able to be made into a Net::ACME2::Error,
152             #then do so to get a nicer error message.
153 0         0 my $acme_error = eval {
154             Net::ACME2::Error->new(
155 0         0 %{ JSON::decode_json( $exc->get('content') ) },
  0         0  
156             );
157             };
158              
159 0 0       0 if ($acme_error) {
160 0         0 die Net::ACME2::X->create(
161             'ACME',
162             {
163             http => $exc,
164             acme => $acme_error,
165             },
166             );
167             }
168             }
169              
170 0         0 $@ = $exc;
171 0         0 die;
172             }
173              
174 4         9 $@ = $eval_err;
175              
176 4         34 return Net::ACME2::HTTP::Response->new($resp);
177             }
178              
179             sub _request_and_set_last_nonce {
180 3     3   14 my ( $self, $type, $url, @args ) = @_;
181              
182 3         14 my $resp = $self->_request( $type, $url, @args );
183              
184             #NB: ACME’s replay protection works thus:
185             # - each server response includes a nonce
186             # - each request must include ONE of the nonces that have been sent
187             # - once used, a nonce can’t be reused
188             #
189             #This is subtly different from what was originally in mind (i.e., that
190             #each request must use the most recently sent nonce). It implies that GETs
191             #do not need to send nonces, though each GET will *receive* a nonce that
192             #may be used.
193 3 50       188 $self->{'_last_nonce'} = $resp->header($_NONCE_HEADER) or do {
194 0         0 die Net::ACME2::X->create('Generic', "Received no $_NONCE_HEADER from $url!");
195             };
196              
197 3         90 return $resp;
198             }
199              
200             sub _get_first_nonce {
201 1     1   7 my ($self) = @_;
202              
203 1 50       9 my $url = $self->{'_nonce_url'} or do {
204              
205             # Shouldn’t happen unless there’s an errant refactor.
206 0         0 die Net::ACME2::X->create('Set newNonce URL first!');
207             };
208              
209 1         7 $self->_request_and_set_last_nonce( 'HEAD', $url );
210              
211 1         21 return;
212             }
213              
214             sub _create_jwt {
215 2     2   6 my ( $self, $jwt_method, $url, $data ) = @_;
216              
217 2 100       33 $self->_get_first_nonce() if !$self->{'_last_nonce'};
218              
219 2   66     12 $self->{'_jwt_maker'} ||= do {
220 1         2 my $class;
221              
222 1 50       14 if ($self->{'_acme_key'}->isa('Crypt::Perl::RSA::PrivateKey')) {
    0          
223 1         3 $class = 'Net::ACME2::JWTMaker::RSA';
224             }
225             elsif ($self->{'_acme_key'}->isa('Crypt::Perl::ECDSA::PrivateKey')) {
226 0         0 $class = 'Net::ACME2::JWTMaker::ECC';
227             }
228             else {
229              
230             # As of this writing, Crypt::Perl only does RSA and ECDSA keys.
231             # If we get here, it’s possible that Crypt::Perl now supports
232             # an additional key type that this library doesn’t recognize.
233 0         0 die Net::ACME2::X->create('Generic', "Unrecognized key type: $self->{'_acme_key'}");
234             }
235              
236 1 50       8 if (!$class->can('new')) {
237 1         8 require Module::Load;
238 1         3 Module::Load::load($class);
239             }
240              
241             $class->new(
242             key => $self->{'_acme_key'},
243 1         17 format => $self->{'_jws_format'},
244             );
245             };
246              
247             return $self->{'_jwt_maker'}->$jwt_method(
248             key_id => $self->{'_key_id'},
249             payload => $data,
250             extra_headers => {
251 2         27 nonce => $self->{'_last_nonce'},
252             url => $url,
253             },
254             );
255             }
256              
257             1;