File Coverage

blib/lib/Net/ACME2/HTTP.pm
Criterion Covered Total %
statement 75 94 79.7
branch 12 24 50.0
condition 4 9 44.4
subroutine 19 20 95.0
pod 0 6 0.0
total 110 153 71.9


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