1package Security::TLSCheck::Checks::Web;
2
3use 5.010;
4
5use Carp;
6use English qw( -no_match_vars );
7
8use Moose;
9extends 'Security::TLSCheck::Checks';
10with 'Security::TLSCheck::Checks::Helper::Timing';
11
12use Log::Log4perl::EasyCatch;
13
14use LWP::UserAgent;
15use HTTP::Status qw(HTTP_OK HTTP_INTERNAL_SERVER_ERROR);
16
17# Preload later required libraries (for parallel fork mode)
18use HTTP::Response;
19use HTTP::Request;
20use LWP::Protocol::https;
21use LWP::Protocol::http;
22use Mozilla::CA;
23use IO::Socket::SSL;
24
25
26use Readonly;
27Readonly my $NOT_FOUND => -1;
28
29
30=head1 NAME
31
32Security::TLSCheck::Checks::Web - (Basic) HTTP and HTTPS Checks
33
34=encoding utf8
35
36=cut
37
38use version; our $VERSION = sprintf "%d", q$Revision: 640 $ =~ /(\d+)/xg;
39
40
41=head1 SYNOPSIS
42
43...
44
45
46=head1 DESCRIPTION
47
48This module checks some (basic) HTTP key figures:
49
50  * HTTP / HTTPS for domain or www domain active; status OK?
51  * HTTP redirects to HTTPS?
52  * redirections
53  * simple HTTPS Certificate Verification (via LWP with help from Mozilla::CA)
54
55
56For simplification of the results, this check first tries to use http://www.domain.tld/
57and only if this does gives an result only http://domain.tld/
58
59So we don't have to count two results per domain, only one.
60
61
62=cut
63
64#<<<
65
66{
67my $key_figures =
68   [
69   { name => "HTTP active",               type => "flag",  source => "http_active",            description => "Is there a HTTP server on Port 80? (all Status OK)", },
70   { name => "HTTP OK",                   type => "flag",  source => "http_ok",                description => "HTTP-Server returns Status 200 OK", },
71   { name => "HTTPS active",              type => "flag",  source => "https_active",           description => "Is there a HTTPS server on Port 443? (all states are OK)", },
72   { name => "HTTPS host verified",       type => "flag",  source => "https_host_verified",    description => "HTTPS is active and host matches", },
73   { name => "HTTPS cert verified",       type => "flag",  source => "https_cert_verified",    description => "HTTPS is active and certificate is verified against Mozilla::CA", },
74   { name => "HTTPS wrong host, cert OK", type => "flag",  source => "https_cert_ok_host_not", description => "HTTPS is active but host does not match", },
75   { name => "HTTPS all verified",        type => "flag",  source => "https_all_verified",     description => "HTTPS is active, host matches and certificate is verified against Mozilla::CA", },
76   { name => "HTTPS OK",                  type => "flag",  source => "https_ok",               description => "HTTPS returns Status 200 OK (certificate/host not checked)", },
77   { name => "HTTPS all verified and OK", type => "flag",  source => "https_all_ok",           description => "HTTPS returns Status 200 OK (certificate and host are checked)", },
78   { name => "Redirect to HTTPS",         type => "flag",  source => "redirects_to_https",     description => "HTTP redirects to HTTPS", },
79   { name => "Redirect to HTTP",          type => "flag",  source => "redirects_to_http",      description => "HTTPS redirects to HTTP", },
80   { name => "Supports HSTS",             type => "flag",  source => "hsts_max_age",           description => "Supports HTTP Strict Transport Security", },
81   { name => "HSTS max age",              type => "int",   source => "hsts_max_age",           description => "How long browsers should cache HTTP Strict Transport Security", },
82   { name => "Disables HSTS",             type => "flag",  source => "disables_hsts",          description => "HTTP Strict Transport Security is disabled by server", },
83   { name => "Used cipher suite",         type => "group", source => "cipher_suite",           description => "The cipher suite, selected by the server", },
84   { name => "Certificate issuer",        type => "group", source => "cert_issuer",            description => "Issuer of the certificate", },
85   { name => "Certificate Let's Encrypt", type => "flag",  source => "cert_letsencrypt",       description => "Issuer of the certificate is Let's Encrypt", },
86   { name => "Certificate self-signed",   type => "flag",  source => "cert_selfsigned",        description => "Certificate is self-signed", },
87   { name => "Cert self-signed, host OK", type => "flag",  source => "cert_selfsigned_hostok", description => "Certificate is self-signed and host matches", },
88   { name => "Server full string",        type => "group", source => "server",                 description => "The full server string (HTTP)", },
89   { name => "Server name",               type => "group", source => "server_name",            description => "The server name string", },
90   { name => "Server name/major version", type => "group", source => "server_major_version",   description => "The server name and major version", },
91   { name => "Supports HPKP",             type => "flag",  source => "has_hpkp",               description => "Server has a public key pinng header", },
92   { name => "Supports HPKP report",      type => "flag",  source => "has_hpkp_report",        description => "Server has a report-only public key pinng header", },
93   ];
94
95
96has '+key_figures' => ( default => sub {return $key_figures} );
97}
98
99has '+description' => ( default => "(Basic) HTTP and HTTPS Checks" );
100
101has _ua                       => ( is => "ro", isa => "LWP::UserAgent", lazy_build => 1, );
102has _http_response            => ( is => "ro", isa => "HTTP::Response", lazy_build => 1, );
103has _https_response           => ( is => "ro", isa => "HTTP::Response", lazy_build => 1, );
104has _https_response_hostcheck => ( is => "ro", isa => "HTTP::Response", lazy_build => 1, );
105has _https_response_certcheck => ( is => "ro", isa => "HTTP::Response", lazy_build => 1, );
106has _https_response_nocheck   => ( is => "ro", isa => "HTTP::Response", lazy_build => 1, );
107
108
109#>>>
110
111
112sub _build__ua
113   {
114   my $self = shift;
115   return LWP::UserAgent->new( timeout => $self->timeout, agent => $self->user_agent_name, );
116   }
117
118sub _build__http_response
119   {
120   my $self = shift;
121   return $self->_do_request("http");
122   }
123
124sub _build__https_response
125   {
126   my $self = shift;
127   return $self->_do_request("https");
128   }
129
130sub _build__https_response_hostcheck
131   {
132   my $self = shift;
133   return $self->_do_request( "https", { verify_hostname => 1, SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE } );
134   }
135
136sub _build__https_response_certcheck
137   {
138   my $self = shift;
139   return $self->_do_request( "https", { verify_hostname => 0, SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER } );
140   }
141
142sub _build__https_response_nocheck
143   {
144   my $self = shift;
145   return $self->_do_request( "https", { verify_hostname => 0, SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE } );
146   }
147
148sub _do_request
149   {
150   my $self     = shift;
151   my $protocol = shift;
152   my $ssl_opts = shift // { verify_hostname => 1, SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER };
153
154   my $ua = $self->_ua;
155   $ua->ssl_opts(%$ssl_opts);
156
157   # If check with www. if not successful, try without www
158   # if this is also not successful, use the www result!
159   my $response = $ua->get( "$protocol://" . $self->www );
160   unless ( $response->is_success )
161      {
162      my $domain_response = $ua->get( "$protocol://" . $self->domain );
163      $response = $domain_response if $domain_response->is_success;
164      }
165
166   return $response;
167   }
168
169
170
171=head1 METHODS
172
173=head2 http_active, https_active, https_all_verified, https_host_verified, https_cert_verified, https_cert_ok_host_not
174
175Checks, if there is something on port 80/443 ...
176
177Fails when Status is 500 and there is a "Client-Warning" header with "Internal response"
178
179   * https_active is true, if there is HTTPS, certificate verify failed, but there is https.
180   * https_all_verified is only true, if certificate verification is OK and host matches.
181   * https_host_verified is true, if the SSL host matches, but cert is not checked
182   * https_cert_verified is true, if there is a valid certificate (Mozilla::CA), hostname not checked
183   * https_cert_ok_host_not is true, if there is a valid certificate, but hostname does NOT match
184
185=cut
186
187#sub _has_https
188#   {
189#   my $self = shift;
190#   return 1 if  _valid_response( $self->_https_response_nocheck );
191#   return;
192#   }
193
194sub _valid_response
195   {
196   my $response = shift;
197
198   return 1 unless $response->code == HTTP_INTERNAL_SERVER_ERROR;
199   return 1 unless defined $response->header("Client-Warning");
200   return 1 unless $response->header("Client-Warning") eq "Internal response";
201   return 0;
202   }
203
204sub http_active
205   {
206   my $self = shift;
207   return _valid_response( $self->_http_response );
208   }
209
210sub https_active
211   {
212   my $self = shift;
213
214   return 1 if _valid_response( $self->_https_response_nocheck );
215
216   # Old method; still active because tests can't check ssl_opts
217   #   return 1 if $self->https_all_verified;
218   # return 1 if index( $self->_https_response->status_line, "certificate verify failed" ) != $NOT_FOUND;
219   return 0;
220   }
221
222
223
224sub https_all_verified
225   {
226   my $self = shift;
227   return unless $self->https_active;              # when no HTTPS active, don't check more HTTPS
228   return _valid_response( $self->_https_response );
229   }
230
231sub https_host_verified
232   {
233   my $self = shift;
234   return unless $self->https_active;              # when no HTTPS active, don't check more HTTPS
235   return _valid_response( $self->_https_response_hostcheck );
236   }
237
238sub https_cert_verified
239   {
240   my $self = shift;
241   return unless $self->https_active;              # when no HTTPS active, don't check more HTTPS
242   return _valid_response( $self->_https_response_certcheck );
243   }
244
245sub https_cert_ok_host_not
246   {
247   my $self = shift;
248   return unless $self->https_active;              # when no HTTPS active, don't check more HTTPS
249   return 1 if $self->https_cert_verified and not $self->https_host_verified;
250   return 0;
251   }
252
253=head2 http_ok, https_ok, https_all_ok
254
255Returns true if HTTP request was sucessful and no error (status Code 2xx)
256
257=cut
258
259sub http_ok
260   {
261   my $self = shift;
262   return $self->_http_response->is_success;
263   }
264
265sub https_ok
266   {
267   my $self = shift;
268   return $self->_https_response_nocheck->is_success;
269   }
270
271sub https_all_ok
272   {
273   my $self = shift;
274   return unless $self->https_active;              # when no HTTPS active, don't check more HTTPS
275   return $self->_https_response->is_success;
276   }
277
278=head2 redirects_to_https
279
280Returns true, all HTTP Requests (on the start page) are redirected to HTTPS
281
282=cut
283
284sub redirects_to_https
285   {
286   my $self = shift;
287
288   #use Data::Dumper;
289
290   #   DEBUG "Redir to HTTPSSSS? Domain: " . $self->domain;
291   #   DEBUG Dumper( $self->_http_response );
292   #
293   #
294
295   # look at the last request in the HTTP request chain; if there
296   # was a redirect to HTTPS, then there is the URI ...
297   return 1 if $self->_http_response->request->uri =~ m{^https}x;
298   return 0;
299   }
300
301=head2 redirects_to_http
302
303Returns true, if HTTPS Requests (on the startpage) are redirected to HTTP
304
305Checked for all HTTPS conections, including invalid Certs.
306
307
308=cut
309
310sub redirects_to_http
311   {
312   my $self = shift;
313
314   return unless $self->https_active;              # when no HTTPS active, don't check more HTTPS
315
316
317   # use Data::Dumper;
318
319   #   DEBUG "Redir to HTTP? Domain: " . $self->domain;
320   #   DEBUG Dumper( $self->_https_response );
321   #
322
323
324   # look at the last request in the HTTPS request chain; if there
325   # was a redirect to HTTP, then there is the URI ...
326   return 1 if $self->_https_response_nocheck->request->uri =~ m{^http:}x;
327   return 0;
328   }
329
330
331
332=head2 hsts_max_age
333
334Returns the max-age value of the Strict-Transport-Security header.
335
336Checked for all certs (also when invalid).
337
338Returns undef, if there is none.
339
340
341RFC says: The max-age directive value can optionally be quoted:
342
343  Strict-Transport-Security: max-age="31536000"
344
345=cut
346
347sub hsts_max_age
348   {
349   my $self = shift;
350
351   my @hsts = $self->_https_response_nocheck->header("Strict-Transport-Security");
352   return unless @hsts;
353
354   my %hsts = map { _split_hsts($ARG) } map { split( m{\s*;\s*}x, $ARG ) } @hsts;
355
356   DEBUG "Probably parsing error: found a HSTS header, but no max_age for @hsts at " . $self->www unless defined $hsts{"max-age"};
357
358   # remove all non-numbers, because some hsts headers are broken.
359   ( my $filtered_hsts = $hsts{"max-age"} ) =~ s{\D}{}g;
360
361   return $filtered_hsts;
362
363   }
364
365sub _split_hsts
366   {
367   my $param = shift;
368
369   my ( $key, $value ) = $param =~ m{ ^ ([-\w]+) \s* (?: = \s* (.*) )? }x;
370   $value //= "";
371
372   # strip sourrounding " and '
373   $value =~ s{ ^ (["']) (.*) \1 $}{$2}gx;
374
375   return ( lc($key), $value );
376
377   }
378
379
380
381=head2 disables_hsts
382
383Does the site resets HTTP Strict Transport Security?
384
385This is the case, when the max_age is set to 0.
386
387=cut
388
389sub disables_hsts
390   {
391   my $self = shift;
392   my $max_age = $self->hsts_max_age // return 0;
393
394   return 1 if $max_age == 0;
395   return 0;
396   }
397
398
399=head2 cipher_suite
400
401Extracts the used cipher_suite from the HTTP-Headers (Client-SSL-Cipher)
402
403Checked for all HTTPS connections, also checked when invalid certificate
404
405=cut
406
407sub cipher_suite
408   {
409   my $self = shift;
410   return $self->_https_response_nocheck->header("Client-SSL-Cipher");
411   }
412
413=head2 cert_issuer
414
415Extracts certifivate issuer from the HTTP-Headers (Client-SSL-Cert-Issuer)
416
417ONLY FOR VALID CERTS!
418
419=cut
420
421sub cert_issuer
422   {
423   my $self = shift;
424   return unless $self->https_cert_verified;       # only remember issuer, when it's a valid CA
425   return $self->_https_response->header("Client-SSL-Cert-Issuer");
426   }
427
428=head2 ->cert_letsencrypt
429
430Checks, if the cert is signed by Let's Encrypt
431
432=cut
433
434sub cert_letsencrypt
435   {
436   my $self = shift;
437   my $cert_issuer = $self->cert_issuer // return;
438   return index( $cert_issuer, "Let's Encrypt" ) > -1;
439   }
440
441
442=head2 ->cert_selfsigned
443
444Checks, if the cert is selfsigned
445
446=cut
447
448sub cert_selfsigned
449   {
450   my $self         = shift;
451   my $cert_issuer  = $self->_https_response->header("Client-SSL-Cert-Issuer")  // return;
452   my $cert_subject = $self->_https_response->header("Client-SSL-Cert-Subject") // return;
453   return $cert_subject eq $cert_issuer;
454   }
455
456=head2 ->cert_selfsigned_hostok
457
458Checks, if the cert is selfsigned AND the hostname matches
459
460=cut
461
462sub cert_selfsigned_hostok
463   {
464   my $self = shift;
465   return ( $self->https_host_verified and $self->cert_selfsigned );
466   }
467
468
469=head2 server
470
471Extracts server string from Server header.
472
473=cut
474
475sub server
476   {
477   my $self = shift;
478   return $self->_http_response->header("Server");
479   }
480
481=head2 server_name
482
483Server name, without other informations (Version, modules, ...)
484
485=cut
486
487sub server_name
488   {
489   my $self = shift;
490   return _get_server_name( $self->server );
491   }
492
493sub _get_server_name
494   {
495   my $server = shift // return;
496
497   my ($name) = $server =~ m{ ^ ([^/]*) }x;
498   return $name if length($name) < 20;
499
500   $name =~ s{ ( [^\w\s].* ) }{}xg;
501
502   return $name;
503
504   }
505
506
507=head2 server_major_version
508
509Server name and major version, without other informations (patchlevel, modules, ...)
510
511=cut
512
513sub server_major_version
514   {
515   my $self = shift;
516
517   return _get_server_major_version( $self->server );
518
519   }
520
521sub _get_server_major_version
522   {
523   my $server = shift // return;
524
525   my ($name_version) = $server =~ m{ ( [-\w\s]+ (?: / \d+ (?:[.]\d+) )? ) }x;
526
527   return $name_version // _get_server_name($server);
528
529   }
530
531
532=head2 has_hpkp
533
534Supports HTTP Public Key pinning (Public-Key-Pins Header).
535
536Checked for all HTTPS conections, including invalid Certs.
537
538=cut
539
540sub has_hpkp
541   {
542   my $self = shift;
543   return 1 if $self->_https_response_nocheck->header("Public-Key-Pins");
544   }
545
546
547=head2 has_hpkp_report
548
549Supports HTTP Public Key pinning, report only (Public-Key-Pins-Report-Only Header).
550
551Checked for all HTTPS conections, including invalid Certs.
552
553
554=cut
555
556sub has_hpkp_report
557   {
558   my $self = shift;
559   return 1 if $self->_https_response_nocheck->header("Public-Key-Pins-Report-Only");
560   }
561
562
563
564__PACKAGE__->meta->make_immutable;
565
5661;
567
568