htpdate 8.17 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 $done_file;
57
my $res_file;
amnesia's avatar
amnesia committed
58

59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
our ($opt_d, $opt_h, $opt_q, $opt_x, $opt_u, $opt_a, $opt_f, $opt_l, $opt_p, $opt_t, $opt_D, $opt_T);

sub done {
    if (defined $done_file) {
	$> = 0 if $opt_u;
	open my $f, '>', $done_file or
	    print STDERR "Couldn't write done file: $done_file\n";
	close $f;
	$> = getpwnam($opt_u) if $opt_u;
    }
}

$SIG{__DIE__} = sub {
    done;
    die(@_);
};
amnesia's avatar
amnesia committed
75 76 77 78 79

sub message {
    my @msg = @_;

    if ($log) {
amnesia's avatar
amnesia committed
80
        open my $h, '>>', $log or die "Cannot open log file $log: $!";
amnesia's avatar
amnesia committed
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
        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
99 100 101

sub parseCommandLine () {
    # specify valid switches
102
    getopts('dhqxfpu:a:l:t:D:T:') || usage();
amnesia's avatar
amnesia committed
103 104 105 106 107 108 109 110

    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
111
    $log = $opt_l           if $opt_l;
112
    $paranoid = 1           if $opt_p;
amnesia's avatar
amnesia committed
113 114
    $quiet = 1              if $opt_q;
    $set_date = 0           if $opt_x;
115
    $dns_timeout = $opt_t   if $opt_t;
116
    $done_file = $opt_D     if $opt_D;
117
    $res_file = $opt_T      if $opt_T;
amnesia's avatar
amnesia committed
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134

    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
135
Usage: $0 [-dhqxf] [-u userid] [-a useragent] [-t dns_timeout] [-T success_file] <URL> [<URL> ...]
amnesia's avatar
amnesia committed
136 137 138 139 140 141 142 143

        -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
144
        -l      log to this file rather than to STDOUT
145
        -p      paranoid mode: don't set time unless all servers could be reached
146
        -t      DNS timeout for wget
147
        -D      create this file after quitting in any way
148
        -T      create this file after setting time successfully
amnesia's avatar
amnesia committed
149 150 151 152 153 154 155 156 157 158 159

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

USAGE

    exit;
}

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

160
    my @files = grep { ! ( $_ =~ m|/?\.{1,2}$| ) } glob("$dir/.* $dir/*");
amnesia's avatar
amnesia committed
161
    @files or error "No downloaded files can be found";
amnesia's avatar
amnesia committed
162 163 164 165 166 167

    my $newestdt;

    foreach my $file (@files) {
        next if -l $file || -d _;
        my $date;
amnesia's avatar
amnesia committed
168
        open(my $file_h, '<', $file) or die "Can not read file $file: $!";
amnesia's avatar
amnesia committed
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
        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
191
    defined $url or error "getRemoteDateDiff must be passed an URL";
amnesia's avatar
amnesia committed
192 193
    $fullrequest = defined $fullrequest ? $fullrequest : 0;

194
    my $tmpdir = tempdir("XXXXXXXXXX", TMPDIR => 1);
amnesia's avatar
amnesia committed
195 196 197 198 199 200

    my @wget_options = ( '-U', $useragent, '--quiet', '--no-cache',
                         '-e', 'robots=off', '--save-headers',
                         '--no-directories',
                         '--secure-protocol', $ssl_protocol,
                     );
201
    push @wget_options, ('--dns-timeout', $dns_timeout) if defined $dns_timeout;
202
    push @wget_options, ('--directory-prefix', $tmpdir);
amnesia's avatar
amnesia committed
203 204 205 206
    if ($fullrequest) {
        push @wget_options, ('--page-requisites', '--span-hosts');
    }

207
    my @cmdline = ('wget', @wget_options, $url);
amnesia's avatar
amnesia committed
208 209 210

    # fetch (the page and) referenced resources:
    # images, stylesheets, scripts, etc.
211
    my $before = DateTime->now->epoch();
amnesia's avatar
amnesia committed
212
    WIFEXITED(system(@cmdline)) or error "Failed to fetch content from $url: $!";
213
    my $local = DateTime->now->epoch();
amnesia's avatar
amnesia committed
214 215 216
    my $newestdt;
    eval { $newestdt = newestDateHeader($tmpdir) };
    if ($EVAL_ERROR =~ m/No downloaded files can be found/) {
amnesia's avatar
amnesia committed
217
        rmtree($tmpdir);
amnesia's avatar
amnesia committed
218
        error "No file could be downloaded from $url.";
amnesia's avatar
amnesia committed
219
    }
amnesia's avatar
amnesia committed
220

amnesia's avatar
amnesia committed
221 222
    rmtree($tmpdir);

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

226 227
    my $diff = $newest_epoch - $local;
    my $took = $local - $before;
228

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

231
    return $diff;
amnesia's avatar
amnesia committed
232 233 234
}

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

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

239 240
    my $local = DateTime->now->epoch();
    my $absdiff = abs($diff);
amnesia's avatar
amnesia committed
241

amnesia's avatar
amnesia committed
242
    debug("Median diff: $diff second(s)");
amnesia's avatar
amnesia committed
243

244
    if ( $maxadjust && $absdiff gt $maxadjust ) {
amnesia's avatar
amnesia committed
245
        message("Not setting clock as diff ($diff seconds) is too large.");
amnesia's avatar
amnesia committed
246
    }
247
    elsif ( $absdiff lt $minadjust) {
amnesia's avatar
amnesia committed
248
        message("Not setting clock as diff ($diff seconds) is too small.");
amnesia's avatar
amnesia committed
249 250
    }
    else {
amnesia's avatar
amnesia committed
251
        my $newtime = DateTime->now->epoch + $diff;
amnesia's avatar
amnesia committed
252
        message("Setting time to $newtime...");
amnesia's avatar
amnesia committed
253 254
        if ($set_date) {
            $> = 0 if $opt_u;
amnesia's avatar
amnesia committed
255 256
            open(my $fd, "-|", $datecommand, $dateparam, '@' . $newtime)
                or die "Cannot set run command $datecommand: $!";
amnesia's avatar
amnesia committed
257
            if ( $? != 0 ) {
amnesia's avatar
amnesia committed
258 259
                my @output = <$fd>;
                error "An error occured setting the time\n@output";
amnesia's avatar
amnesia committed
260 261 262 263 264
            }
            close($fd);
            $> = getpwnam($opt_u) if $opt_u;
        }
    }
265 266 267 268 269
    $> = 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
270 271 272
}

my @urls = parseCommandLine();
273
message("Running htpdate.");
274
my @diffs = grep {
275 276
    defined $_
} map {
277 278
    my $diff = $_->{thread}->join();
    if ($paranoid && ! defined $diff) {
279 280 281 282
        error('Paranoid mode: aborting as one server (',
              $_->{url},
              ') could not be reached');
    }
283
    $diff;
284 285 286 287 288 289
} map {
    {
        url    => $_,
        thread => threads->create(\&getRemoteDateDiff, $_, $fullrequest),
    }
} @urls
amnesia's avatar
amnesia committed
290
    or error "No Date header could be received.";
291 292
my @sorted_diffs = sort @diffs;
adjustDate($sorted_diffs[int(@sorted_diffs / 2)]);
293
done;