htpdate 7.33 KB
Newer Older
amnesia's avatar
amnesia committed
1
2
3
4
5
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
#!/usr/bin/perl
#
# htpdate time poller version 0.9.3
# Copyright (C) 2005 Eddy Vervest
# Copyright (C) 2010 T(A)ILS dev team <amnesia@boum.org>
#
# 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
35
36
37
38
use Fatal qw( open close );
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         = '';
amnesia's avatar
amnesia committed
46
47
48
49
50
51
52
53
54
my $maxadjust   = 1800;         # maximum time step in seconds
my $minadjust   = 1;            # minimum time step in seconds
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

amnesia's avatar
amnesia committed
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
our ($opt_d, $opt_h, $opt_q, $opt_x, $opt_u, $opt_a, $opt_f, $opt_l);

sub message {
    my @msg = @_;

    if ($log) {
        open my $h, '>>', $log;
        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
80
81
82

sub parseCommandLine () {
    # specify valid switches
amnesia's avatar
amnesia committed
83
    getopts('dhqxfu:a:l:') || usage();
amnesia's avatar
amnesia committed
84
85
86
87
88
89
90
91

    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
92
    $log = $opt_l           if $opt_l;
amnesia's avatar
amnesia committed
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
    $quiet = 1              if $opt_q;
    $set_date = 0           if $opt_x;

    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
Usage: $0 [-dhqxf] [-u userid] [-a useragent] <URL> [<URL> ...]

        -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
121
        -l      log to this file rather than to STDOUT
amnesia's avatar
amnesia committed
122
123
124
125
126
127
128
129
130
131
132
133
134
135

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

USAGE

    exit;
}

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

    my $origdir = getcwd;
    chdir $dir;

amnesia's avatar
amnesia committed
136
    my @files = grep { ! ( $_ =~ m/^\.{1,2}$/ ) } glob('.* *');
amnesia's avatar
amnesia committed
137
    @files or error "No downloaded files can be found";
amnesia's avatar
amnesia committed
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167

    my $newestdt;

    foreach my $file (@files) {
        next if -l $file || -d _;
        my $date;
        open(my $file_h, '<', $file);
        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;
            }
        }
    }

    chdir $origdir;
    return $newestdt;
}

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

amnesia's avatar
amnesia committed
168
    defined $url or error "getRemoteDateDiff must be passed an URL";
amnesia's avatar
amnesia committed
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
    $fullrequest = defined $fullrequest ? $fullrequest : 0;

    my $origdir = getcwd;

    my @wget_options = ( '-U', $useragent, '--quiet', '--no-cache',
                         '-e', 'robots=off', '--save-headers',
                         '--no-directories',
                         '--secure-protocol', $ssl_protocol,
                     );
    if ($fullrequest) {
        push @wget_options, ('--page-requisites', '--span-hosts');
    }
    my @cmdline = ('wget', @wget_options, $url);

    my $tmpdir = tempdir("XXXXXXXXXX", TMPDIR => 1);
    chdir $tmpdir;

    # fetch (the page and) referenced resources:
    # images, stylesheets, scripts, etc.
amnesia's avatar
amnesia committed
188
    my $beforedt = DateTime->now;
amnesia's avatar
amnesia committed
189
    WIFEXITED(system(@cmdline)) or error "Failed to fetch content from $url: $!";
amnesia's avatar
amnesia committed
190
    my $localdt = DateTime->now;
amnesia's avatar
amnesia committed
191
192
193
    my $newestdt;
    eval { $newestdt = newestDateHeader($tmpdir) };
    if ($EVAL_ERROR =~ m/No downloaded files can be found/) {
amnesia's avatar
amnesia committed
194
        error "No file could be downloaded from $url.";
amnesia's avatar
amnesia committed
195
    }
amnesia's avatar
amnesia committed
196

amnesia's avatar
amnesia committed
197
    defined $newestdt or error "Could not get any Date header";
amnesia's avatar
amnesia committed
198
199
200

    my $diffdt = $newestdt - $localdt;
    my $diff   = $diffdt->in_units('seconds');
amnesia's avatar
amnesia committed
201
202
203
    my $tookdt = $localdt - $beforedt;
    my $took   = $tookdt->in_units('seconds');
    debug("$url (took ${took}s) => diff = $diff second(s)");
amnesia's avatar
amnesia committed
204
205
206
207
208
209
210
211

    chdir $origdir;
    return $diffdt;
}

sub adjustDate {
    my ($diffdt) = @_;

amnesia's avatar
amnesia committed
212
    defined $diffdt or error "adjustDate was passed an undefined diff";
amnesia's avatar
amnesia committed
213
214
215
216
217

    my $localdt = DateTime->now;
    my $absdiffdt = $diffdt->is_positive() ? $diffdt : $diffdt->inverse();
    my $diff      = $diffdt->in_units('seconds');

amnesia's avatar
amnesia committed
218
    debug("Median diff: $diff second(s)");
amnesia's avatar
amnesia committed
219
220
221
222
223

    if ( DateTime::Duration->compare( $absdiffdt,
                                      DateTime::Duration->new(seconds => $maxadjust),
                                      $localdt )
          > 0 ) {
amnesia's avatar
amnesia committed
224
        message("Not setting clock as diff ($diff seconds) is too large.");
amnesia's avatar
amnesia committed
225
226
227
228
229
    }
    elsif ( DateTime::Duration->compare($absdiffdt,
                                        DateTime::Duration->new(seconds => $minadjust),
                                        $localdt, )
          <= 0 ) {
amnesia's avatar
amnesia committed
230
        message("Not setting clock as diff ($diff seconds) is too small.");
amnesia's avatar
amnesia committed
231
232
233
234
    }
    else {
        my $newtimedt = DateTime->now + $diffdt;
        my $newtime = scalar localtime($newtimedt->epoch);
amnesia's avatar
amnesia committed
235
        message("Setting time to $newtime...");
amnesia's avatar
amnesia committed
236
237
238
239
        if ($set_date) {
            $> = 0 if $opt_u;
            open(my $fd, "-|", $datecommand, $dateparam, $newtime);
            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
247
248
249
            }
            close($fd);
            $> = getpwnam($opt_u) if $opt_u;
        }
    }
}

my @urls = parseCommandLine();
amnesia's avatar
amnesia committed
250
251
252
253
254
my @diffdts = grep { defined $_
                 } map { $_->join()
                     } map { threads->create(\&getRemoteDateDiff, $_, $fullrequest)
                         } @urls
    or error "No Date header could be received.";
amnesia's avatar
amnesia committed
255
256
257
258
my @sorted_diffdts = sort {
    $a->in_units('seconds') <=> $b->in_units('seconds')
} @diffdts;
adjustDate($sorted_diffdts[int(@sorted_diffdts / 2)]);