htpdate 7.79 KB
Newer Older
amnesia's avatar
amnesia committed
1 2 3 4
#!/usr/bin/perl
#
# htpdate time poller version 0.9.3
# Copyright (C) 2005 Eddy Vervest
5
# Copyright (C) 2010-2011 Tails developers <tails@boum.org>
amnesia's avatar
amnesia committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# http://www.gnu.org/copyleft/gpl.html

# Proxy setting are read from environment 
# e.g. in bash for setting environment variables:
#
# export HTTP_PROXY='http://wwwproxy.xs4all.nl:8080'
#
# or set the proxy value here
#
# $ENV{HTTP_PROXY} = 'http://wwwproxy.xs4all.nl:8080';
#
# If proxy authentication is required, specify your userid and password below.

use strict;
use warnings;

use version; our $VERSION = qv('0.9.3');

use Carp;
use Cwd;
use DateTime;
use DateTime::Format::DateParse;
amnesia's avatar
amnesia committed
33
use English qw( -no_match_vars );
amnesia's avatar
amnesia committed
34
use File::Path qw(rmtree);
amnesia's avatar
amnesia committed
35 36 37 38
use File::Temp qw/tempdir/;
use Getopt::Std;
use open qw{:utf8 :std};
use POSIX qw( WIFEXITED );
amnesia's avatar
amnesia committed
39
use threads;
amnesia's avatar
amnesia committed
40 41 42 43 44

my $datecommand = '/bin/date';  # "date" command to set time
my $dateparam   = '-s';         # "date" parameter to set time
my $debug       = 0;
my $fullrequest = 0;
amnesia's avatar
amnesia committed
45
my $log         = '';
46
my $maxadjust   = 0;            # maximum time step in seconds (0 means no max.)
amnesia's avatar
amnesia committed
47
my $minadjust   = 1;            # minimum time step in seconds
48
my $paranoid    = 0;
amnesia's avatar
amnesia committed
49 50 51 52 53 54
my $password    = '';           # password for proxy server
my $quiet       = 0;
my $set_date    = 1;
my $ssl_protocol = 'TLSv1';     # will be passed to wget's --secure-protocol
my $useragent   = "htpdate/$VERSION";
my $userid      = '';           # userid for proxy servers
55
my $dns_timeout;
56
my $res_file;
amnesia's avatar
amnesia committed
57

58
our ($opt_d, $opt_h, $opt_q, $opt_x, $opt_u, $opt_a, $opt_f, $opt_l, $opt_p, $opt_t, $opt_T);
amnesia's avatar
amnesia committed
59 60 61 62 63

sub message {
    my @msg = @_;

    if ($log) {
amnesia's avatar
amnesia committed
64
        open my $h, '>>', $log or die "Cannot open log file $log: $!";
amnesia's avatar
amnesia committed
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
        print $h "@msg\n";
        close $h;
    }
    else {
        print "@msg\n" unless $quiet;
    }
}

sub debug {
    message(@_) if $debug;
}

sub error (@_) {
    my @msg = @_;

    debug(@msg);
    croak @msg;
}
amnesia's avatar
amnesia committed
83 84 85

sub parseCommandLine () {
    # specify valid switches
86
    getopts('dhqxfpu:a:l:t:T:') || usage();
amnesia's avatar
amnesia committed
87 88 89 90 91 92 93 94

    usage() if $opt_h;
    usage() unless $ARGV[0];

    $> = getpwnam($opt_u)   if $opt_u;
    $useragent = $opt_a     if $opt_a;
    $debug = 1              if $opt_d;
    $fullrequest = 1        if $opt_f;
amnesia's avatar
amnesia committed
95
    $log = $opt_l           if $opt_l;
96
    $paranoid = 1           if $opt_p;
amnesia's avatar
amnesia committed
97 98
    $quiet = 1              if $opt_q;
    $set_date = 0           if $opt_x;
99
    $dns_timeout = $opt_t   if $opt_t;
100
    $res_file = $opt_T      if $opt_T;
amnesia's avatar
amnesia committed
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117

    my @urls;
    foreach my $url (@ARGV) {
        unless ( $url =~ /^http/i ) {
            $url = 'https://'.$url;
        }
        push @urls, $url;
    }

    return @urls;
}

sub usage () {

    print STDERR <<USAGE;

htpdate version $VERSION
118
Usage: $0 [-dhqxf] [-u userid] [-a useragent] [-t dns_timeout] [-T success_file] <URL> [<URL> ...]
amnesia's avatar
amnesia committed
119 120 121 122 123 124 125 126

        -d      debug
        -h      show this help
        -q      quiet
        -u      userid to run as
        -x      do not set the time (only show)
        -a      http user agent to use
        -f      request the full page and referenced resources rather than only its header
amnesia's avatar
amnesia committed
127
        -l      log to this file rather than to STDOUT
128
        -p      paranoid mode: don't set time unless all servers could be reached
129
        -t      DNS timeout for wget
130
        -T      create this file after setting time successfully
amnesia's avatar
amnesia committed
131 132 133 134 135 136 137 138 139 140 141

        e.g. $0 -x http://www.microsoft.com/ https://check.torproject.org/

USAGE

    exit;
}

sub newestDateHeader {
    my ($dir) = @_;

142
    my @files = grep { ! ( $_ =~ m|/?\.{1,2}$| ) } glob("$dir/.* $dir/*");
amnesia's avatar
amnesia committed
143
    @files or error "No downloaded files can be found";
amnesia's avatar
amnesia committed
144 145 146 147 148 149

    my $newestdt;

    foreach my $file (@files) {
        next if -l $file || -d _;
        my $date;
amnesia's avatar
amnesia committed
150
        open(my $file_h, '<', $file) or die "Can not read file $file: $!";
amnesia's avatar
amnesia committed
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
        while (my $line = <$file_h>) {
            chomp $line;
            # empty line == we leave the headers to go into the content
            last if $line eq '';
            last if ($date) = ($line =~ m/^Date:\s+(.*)$/m);
        }
        close $file_h;
        if (defined $date) {
            # RFC 2616 (3.3.1) says Date headers MUST be represented in GMT
            my $dt = DateTime::Format::DateParse->parse_datetime( $date, 'GMT' );
            if (! defined $newestdt || DateTime->compare($dt, $newestdt) > 0) {
                $newestdt = $dt;
            }
        }
    }

    return $newestdt;
}

sub getRemoteDateDiff {
    my ($url, $fullrequest) = @_;

amnesia's avatar
amnesia committed
173
    defined $url or error "getRemoteDateDiff must be passed an URL";
amnesia's avatar
amnesia committed
174 175
    $fullrequest = defined $fullrequest ? $fullrequest : 0;

176
    my $tmpdir = tempdir("XXXXXXXXXX", TMPDIR => 1);
amnesia's avatar
amnesia committed
177 178 179 180 181 182

    my @wget_options = ( '-U', $useragent, '--quiet', '--no-cache',
                         '-e', 'robots=off', '--save-headers',
                         '--no-directories',
                         '--secure-protocol', $ssl_protocol,
                     );
183
    push @wget_options, ('--dns-timeout', $dns_timeout) if defined $dns_timeout;
184
    push @wget_options, ('--directory-prefix', $tmpdir);
amnesia's avatar
amnesia committed
185 186 187 188
    if ($fullrequest) {
        push @wget_options, ('--page-requisites', '--span-hosts');
    }

189
    my @cmdline = ('wget', @wget_options, $url);
amnesia's avatar
amnesia committed
190 191 192

    # fetch (the page and) referenced resources:
    # images, stylesheets, scripts, etc.
193
    my $before = DateTime->now->epoch();
amnesia's avatar
amnesia committed
194
    WIFEXITED(system(@cmdline)) or error "Failed to fetch content from $url: $!";
195
    my $local = DateTime->now->epoch();
amnesia's avatar
amnesia committed
196 197 198
    my $newestdt;
    eval { $newestdt = newestDateHeader($tmpdir) };
    if ($EVAL_ERROR =~ m/No downloaded files can be found/) {
amnesia's avatar
amnesia committed
199
        rmtree($tmpdir);
amnesia's avatar
amnesia committed
200
        error "No file could be downloaded from $url.";
amnesia's avatar
amnesia committed
201
    }
amnesia's avatar
amnesia committed
202

amnesia's avatar
amnesia committed
203 204
    rmtree($tmpdir);

amnesia's avatar
amnesia committed
205
    defined $newestdt or error "Could not get any Date header";
206
    my $newest_epoch = $newestdt->epoch();
amnesia's avatar
amnesia committed
207

208 209
    my $diff = $newest_epoch - $local;
    my $took = $local - $before;
210

amnesia's avatar
amnesia committed
211
    debug("$url (took ${took}s) => diff = $diff second(s)");
amnesia's avatar
amnesia committed
212

213
    return $diff;
amnesia's avatar
amnesia committed
214 215 216
}

sub adjustDate {
217
    my ($diff) = @_;
amnesia's avatar
amnesia committed
218

219
    defined $diff or error "adjustDate was passed an undefined diff";
amnesia's avatar
amnesia committed
220

221 222
    my $local = DateTime->now->epoch();
    my $absdiff = abs($diff);
amnesia's avatar
amnesia committed
223

amnesia's avatar
amnesia committed
224
    debug("Median diff: $diff second(s)");
amnesia's avatar
amnesia committed
225

226
    if ( $maxadjust && $absdiff gt $maxadjust ) {
amnesia's avatar
amnesia committed
227
        message("Not setting clock as diff ($diff seconds) is too large.");
amnesia's avatar
amnesia committed
228
    }
229
    elsif ( $absdiff lt $minadjust) {
amnesia's avatar
amnesia committed
230
        message("Not setting clock as diff ($diff seconds) is too small.");
amnesia's avatar
amnesia committed
231 232
    }
    else {
amnesia's avatar
amnesia committed
233
        my $newtime = DateTime->now->epoch + $diff;
amnesia's avatar
amnesia committed
234
        message("Setting time to $newtime...");
amnesia's avatar
amnesia committed
235 236
        if ($set_date) {
            $> = 0 if $opt_u;
amnesia's avatar
amnesia committed
237 238
            open(my $fd, "-|", $datecommand, $dateparam, '@' . $newtime)
                or die "Cannot set run command $datecommand: $!";
amnesia's avatar
amnesia committed
239
            if ( $? != 0 ) {
amnesia's avatar
amnesia committed
240 241
                my @output = <$fd>;
                error "An error occured setting the time\n@output";
amnesia's avatar
amnesia committed
242 243 244 245 246
            }
            close($fd);
            $> = getpwnam($opt_u) if $opt_u;
        }
    }
247 248 249 250 251
    $> = 0 if $opt_u;
    open my $res_h, '>>', $res_file or die "Cannot open res file $res_file: $!";
    print $res_h "$diff\n";
    close $res_h;
    $> = getpwnam($opt_u) if $opt_u;
amnesia's avatar
amnesia committed
252 253 254
}

my @urls = parseCommandLine();
255
message("Running htpdate.");
256
my @diffs = grep {
257 258
    defined $_
} map {
259 260
    my $diff = $_->{thread}->join();
    if ($paranoid && ! defined $diff) {
261 262 263 264
        error('Paranoid mode: aborting as one server (',
              $_->{url},
              ') could not be reached');
    }
265
    $diff;
266 267 268 269 270 271
} map {
    {
        url    => $_,
        thread => threads->create(\&getRemoteDateDiff, $_, $fullrequest),
    }
} @urls
amnesia's avatar
amnesia committed
272
    or error "No Date header could be received.";
273 274
my @sorted_diffs = sort @diffs;
adjustDate($sorted_diffs[int(@sorted_diffs / 2)]);