Cosmetics for generate_creation_time()
[privoxy.git] / tools / uagen.pl
1 #!/usr/bin/perl
2
3 ##############################################################################################
4 # uagen (http://www.fabiankeil.de/sourcecode/uagen/)
5 #
6 # $Id: uagen.pl,v 1.15 2011/06/29 20:29:29 fabiankeil Exp $
7 #
8 # Generates a pseudo-random Firefox user agent and writes it into a Privoxy action file
9 # and optionally into a Mozilla prefs file. For documentation see 'perldoc uagen(.pl)'.
10 #
11 # Examples (created with v1.0):
12 #
13 # Mozilla/5.0 (X11; U; NetBSD i386; en-US; rv:1.8.0.2) Gecko/20060421 Firefox/1.5.0.2
14 # Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-CA; rv:1.8.0.2) Gecko/20060425 Firefox/1.5.0.2
15 # Mozilla/5.0 (X11; U; SunOS i86pc; no-NO; rv:1.8.0.2) Gecko/20060420 Firefox/1.5.0.2
16 # Mozilla/5.0 (X11; U; Linux x86_64; de-AT; rv:1.8.0.2) Gecko/20060422 Firefox/1.5.0.2
17 # Mozilla/5.0 (X11; U; NetBSD i386; en-US; rv:1.8.0.2) Gecko/20060415 Firefox/1.5.0.2
18 # Mozilla/5.0 (X11; U; OpenBSD sparc64; pl-PL; rv:1.8.0.2) Gecko/20060429 Firefox/1.5.0.2
19 # Mozilla/5.0 (X11; U; Linux i686; en-CA; rv:1.8.0.2) Gecko/20060413 Firefox/1.5.0.2
20 #
21 # Copyright (c) 2006-2011 Fabian Keil <fk@fabiankeil.de>
22 #
23 # Permission to use, copy, modify, and distribute this software for any
24 # purpose with or without fee is hereby granted, provided that the above
25 # copyright notice and this permission notice appear in all copies.
26 #
27 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
28 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
29 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
30 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
31 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
32 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
33 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
34 ##############################################################################################
35
36 use strict;
37 use warnings;
38 use Time::Local;
39 use Getopt::Long;
40
41 use constant {
42
43    UAGEN_VERSION       => 'uagen 1.1',
44
45    UAGEN_LOGFILE       => '/var/log/uagen.log',
46    ACTION_FILE         => '/etc/privoxy/user-agent.action',
47    MOZILLA_PREFS_FILE  => '',
48    SILENT              =>  0,
49    NO_LOGGING          =>  0,
50    NO_ACTION_FILE      =>  0,
51    LOOP                =>  0,
52    SLEEPING_TIME       =>  5,
53
54    # As of Firefox 4, the "Gecko token" has been frozen
55    # http://hacks.mozilla.org/2010/09/final-user-agent-string-for-firefox-4/
56    RANDOMIZE_RELEASE_DATE => 0,
57
58    # These variables belong together. If you only change one of them, the generated
59    # User-Agent might be invalid. If you're not sure which values make sense,
60    # are too lazy to check, but want to change them anyway, take the values you
61    # see in the "Help/About Mozilla Firefox" menu.
62
63    BROWSER_VERSION                   => "5.0",
64    BROWSER_REVISION                  => '5.0',
65    BROWSER_RELEASE_DATE              => '20100101',
66 };
67
68 use constant LANGUAGES => qw(
69    en-AU en-GB en-CA en-NZ en-US en-ZW es-ES de-DE de-AT de-CH fr-FR sk-SK nl-NL no-NO pl-PL
70 );
71
72 #######################################################################################
73
74 sub generate_creation_time($) {
75     my $release_date = shift;
76
77     my ($rel_year, $rel_mon, $rel_day);
78     my ($c_day, $c_mon, $c_year);
79     my $now = time;
80     my (undef, undef, undef, $mday, $mon, $year, undef, undef, undef) = localtime($now);
81     $mon  += 1;
82     $year += 1900;
83
84     unless ($release_date =~ m/\d{6}/) {
85         log_error("Invalid release date format: $release_date. Using "
86                   . BROWSER_RELEASE_DATE . " instead.");
87         $release_date = BROWSER_RELEASE_DATE;
88     }
89     $rel_year = substr($release_date, 0, 4);
90     $rel_mon  = substr($release_date, 4, 2);
91     $rel_day  = substr($release_date, 6, 2);
92
93     #1, 2, 3, Check.
94     die "release year in the future" if ($year < $rel_year);
95     die "release month in the future"
96       if (($year == $rel_year) and ($mon < $rel_mon));
97     die "release day in the future"
98       if (($year == $rel_year) and ($mon == $rel_mon) and ($mday < $rel_day));
99
100     my @c_time = (0, 0, 0, $rel_day, $rel_mon - 1, $rel_year - 1900, 0, 0, 0);
101     my $c_seconds = timelocal(@c_time);
102
103     $c_seconds = $now - (int rand ($now - $c_seconds));
104     @c_time = localtime($c_seconds);
105     (undef, undef, undef, $c_day, $c_mon, $c_year, undef, undef, undef) = @c_time;
106     $c_mon  += 1;
107     $c_year += 1900;
108
109     #3, 2, 1, Test.
110     die "Compilation year in the future" if ($year < $c_year);
111     die "Compilation month in the future"
112       if (($year == $c_year) and ($mon < $c_mon));
113     die "Compilation day in the future"
114       if (($year == $c_year) and ($mon == $c_mon) and ($mday < $c_day));
115
116     return sprintf("%.2i%.2i%.2i", $c_year, $c_mon, $c_day);
117 }
118
119 sub generate_language_settings() {
120
121     our @languages;
122
123     my $language_i      = int rand (@languages);
124     my $accept_language = $languages[$language_i];
125     $accept_language =~ tr/[A-Z]/[a-z]/;
126
127     return ($languages[$language_i], $accept_language);
128 }
129
130 sub generate_platform_and_os() {
131
132     my %os_data = (
133         FreeBSD => {
134             karma             => 1,
135             platform          => 'X11',
136             architectures     => [ 'i386', 'amd64', 'sparc64', 'alpha' ],
137             order_is_inversed => 0,
138         },
139         OpenBSD => {
140             karma             => 1,
141             platform          => 'X11',
142             architectures     => [ 'i386', 'amd64', 'sparc64', 'alpha' ],
143             order_is_inversed => 0,
144         },
145         NetBSD => {
146             karma             => 1,
147             platform          => 'X11',
148             architectures     => [ 'i386', 'amd64', 'sparc64', 'alpha' ],
149             order_is_inversed => 0,
150         },
151         Linux => {
152             karma             => 1,
153             platform          => 'X11',
154             architectures     => [ 'i586', 'i686', 'x86_64' ],
155             order_is_inversed => 0,
156         },
157         SunOS => {
158             karma             => 1,
159             platform          => 'X11',
160             architectures     => [ 'i86pc', 'sun4u' ],
161             order_is_inversed => 0,
162         },
163         'Mac OS X' => {
164             karma             => 1,
165             platform          => 'Macintosh',
166             architectures     => [ 'PPC', 'Intel' ],
167             order_is_inversed => 1,
168         },
169         Windows => {
170             karma             => 0,
171             platform          => 'Windows',
172             architectures     => [ 'NT 5.1' ],
173             order_is_inversed => 0,
174         }
175     );
176
177     my @os_names;
178
179     foreach my $os_name ( keys %os_data ) {
180         push @os_names, ($os_name) x $os_data{$os_name}{'karma'}
181           if $os_data{$os_name}{'karma'};
182     }
183
184     my $os_i   = int rand(@os_names);
185     my $os     = $os_names[$os_i];
186     my $arch_i = int rand( @{ $os_data{$os}{'architectures'} } );
187     my $arch   = $os_data{$os}{'architectures'}[$arch_i];
188
189     my $platform = $os_data{$os}{'platform'};
190
191     my $os_or_cpu;
192     $os_or_cpu = sprintf "%s %s",
193       $os_data{$os}{'order_is_inversed'} ? ( $arch, $os ) : ( $os, $arch );
194
195     return $platform, $os_or_cpu;
196 }
197
198 sub generate_firefox_user_agent() {
199
200     our $languages;
201     our $browser_version;
202     our $browser_revision;
203     our $browser_release_date;
204     our $randomize_release_date;
205
206     my $mozillaversion  = '5.0';
207     my $security        = "U";
208
209     my $creation_time = $randomize_release_date ?
210         generate_creation_time($browser_release_date) : $browser_release_date;
211     my ( $locale,   $accept_language ) = generate_language_settings();
212     my ( $platform, $os_or_cpu )       = generate_platform_and_os;
213
214     my $firefox_user_agent =
215       sprintf "Mozilla/%s (%s; %s; %s; %s; rv:%s) Gecko/%s Firefox/%s",
216       $mozillaversion, $platform, $security, $os_or_cpu, $locale, $browser_revision,
217       $creation_time, $browser_version;
218
219     return $accept_language, $firefox_user_agent;
220 }
221
222 sub log_to_file($) {
223
224     my $message = shift;
225
226     our $logfile;
227     our $no_logging;
228
229     my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
230       localtime time;
231     $year += 1900;
232     $mon  += 1;
233     my $logtime = sprintf "%i/%.2i/%.2i %.2i:%.2i", $year, $mon, $mday, $hour,
234       $min;
235
236     return if $no_logging;
237
238     open(my $log_fd, ">>" . $logfile) || die "Writing " . $logfile . " failed";
239     printf $log_fd UAGEN_VERSION . " ($logtime) $message\n";
240     close($log_fd);
241 }
242
243 sub log_error($) {
244
245     my $message = shift;
246
247     $message = "Error: $message";
248     log_to_file($message);
249     print "$message\n";
250
251     exit(1);
252 }
253
254 sub write_action_file() {
255
256     our $action_file;
257     our $user_agent;
258     our $accept_language;
259     our $no_hide_accept_language;
260     our $action_injection;
261
262     my $action_file_content = '';
263
264     if ($action_injection){
265         open(my $actionfile_fd, "<", $action_file)
266             or log_error "Reading action file $action_file failed!";
267         while (<$actionfile_fd>) {
268             s@(hide-accept-language\{).*?(\})@$1$accept_language$2@;
269             s@(hide-user-agent\{).*?(\})@$1$user_agent$2@;
270             $action_file_content .= $_;
271         }
272         close($actionfile_fd);
273     } else {
274         $action_file_content = "{";
275         $action_file_content .= sprintf "+hide-accept-language{%s} \\\n",
276             $accept_language unless $no_hide_accept_language;
277         $action_file_content .= sprintf " +hide-user-agent{%s} \\\n}\n/\n",
278             $user_agent;
279     }
280     open(my $actionfile_fd, ">" . $action_file)
281       or log_error "Writing action file $action_file failed!";
282     print $actionfile_fd $action_file_content;
283     close($actionfile_fd);
284
285     return 0;
286 }
287
288 sub write_prefs_file() {
289
290     our $mozilla_prefs_file;
291     our $user_agent;
292     our $accept_language;
293     our $clean_prefs;
294
295     my $prefs_file_content = '';
296     my $prefsfile_fd;
297
298     if (open($prefsfile_fd, $mozilla_prefs_file)) {
299
300         while (<$prefsfile_fd>) {
301             s@user_pref\(\"general.useragent.override\",.*\);\n?@@;
302             s@user_pref\(\"intl.accept_languages\",.*\);\n?@@;
303             $prefs_file_content .= $_;
304         }
305         close($prefsfile_fd);
306     } else {
307         log_error "Reading prefs file $mozilla_prefs_file failed. Creating a new file!";
308     }
309
310     $prefs_file_content .=
311         sprintf("user_pref(\"general.useragent.override\", \"%s\");\n", $user_agent) .
312         sprintf("user_pref(\"intl.accept_languages\", \"%s\");\n", $accept_language)
313         unless $clean_prefs;
314
315     open($prefsfile_fd, ">" . $mozilla_prefs_file)
316       or log_error "Writing prefs file $mozilla_prefs_file failed!";
317     print $prefsfile_fd $prefs_file_content;
318     close($prefsfile_fd);
319
320 }
321
322 sub VersionMessage() {
323     printf UAGEN_VERSION . "\n" . 'Copyright (C) 2006-2011 Fabian Keil <fk@fabiankeil.de> ' .
324         "\nhttp://www.fabiankeil.de/sourcecode/uagen/\n";
325 }
326
327 sub help() {
328
329     our $logfile;
330     our $action_file;
331     our $browser_version;
332     our $browser_revision;
333     our $browser_release_date;
334     our $sleeping_time;
335     our $loop;
336     our $mozilla_prefs_file;
337
338     my $comma_separated_languages;
339
340     $loop = $loop ? ' ' . $loop : '';
341     $mozilla_prefs_file = $mozilla_prefs_file ? ' ' . $mozilla_prefs_file : '';
342     foreach (LANGUAGES){
343         $comma_separated_languages .= $_ . ",";
344     }
345     chop $comma_separated_languages;
346
347     VersionMessage;
348
349     print << "    EOF"
350
351 Options and their default values if there are any:
352     [--action-file $action_file]
353     [--action-injection]
354     [--browser-release-date $browser_release_date]
355     [--browser-revision $browser_revision]
356     [--browser-version $browser_version]
357     [--clean-prefs-file]
358     [--help]
359     [--language-overwrite $comma_separated_languages]
360     [--logfile $logfile]
361     [--loop$loop]
362     [--no-action-file]
363     [--no-hide-accept-language]
364     [--no-logfile]
365     [--prefs-file$mozilla_prefs_file]
366     [--randomize-release-date]
367     [--quiet]
368     [--silent]
369     [--sleeping-time $sleeping_time]
370     [--version]
371 see "perldoc $0" for more information
372     EOF
373     ;
374     exit(0);
375 }
376
377 sub main() {
378
379     my $error_message;
380     my  $no_action_file          = NO_ACTION_FILE;
381
382     our $silent                  = SILENT;
383     our $no_logging              = NO_LOGGING;
384     our $logfile                 = UAGEN_LOGFILE;
385     our $action_file             = ACTION_FILE;
386     our $randomize_release_date  = RANDOMIZE_RELEASE_DATE;
387     our $browser_version         = BROWSER_VERSION;
388     our $browser_revision        = BROWSER_REVISION;
389     our $browser_release_date    = BROWSER_RELEASE_DATE;
390     our $sleeping_time           = SLEEPING_TIME;
391     our $loop                    = LOOP;
392     our $no_hide_accept_language = 0;
393     our $action_injection        = 0;
394
395     our @languages;
396     our ( $accept_language, $user_agent );
397     our $mozilla_prefs_file = MOZILLA_PREFS_FILE;
398     our $clean_prefs = 0;
399
400     GetOptions('logfile=s' => \$logfile,
401                'action-file=s' => \$action_file,
402                'language-overwrite=s@' => \@languages,
403                'silent|quiet' => \$silent,
404                'no-hide-accept-language' => \$no_hide_accept_language,
405                'no-logfile' => \$no_logging,
406                'no-action-file' => \$no_action_file,
407                'randomize-release-date' => \$randomize_release_date,
408                'browser-version=s' => \$browser_version,
409                'browser-revision=s' => \$browser_revision,
410                'browser-release-date=s' => \$browser_release_date,
411                'action-injection' => \$action_injection,
412                'loop' => \$loop,
413                'sleeping-time' => \$sleeping_time,
414                'prefs-file=s' => \$mozilla_prefs_file,
415                'clean-prefs-file' => \$clean_prefs,
416                'help' => \&help,
417                'version' => sub {VersionMessage() && exit(0)}
418     ) or exit(0);
419
420     if (@languages) {
421         @languages = split(/,/,join(',',@languages));
422     } else {
423         @languages = LANGUAGES;
424     }
425
426     srand( time ^ ( $$ + ( $$ << 15 ) ) );
427
428     do {
429         $error_message='';
430         ( $accept_language, $user_agent ) = generate_firefox_user_agent();
431
432         print "$user_agent\n" unless $silent;
433
434         write_action_file() unless $no_action_file;
435         write_prefs_file() if $mozilla_prefs_file;
436
437         log_to_file "Generated User-Agent: $user_agent";
438
439     } while ($loop && sleep($sleeping_time * 60));
440 }
441
442 main();
443 exit(0);
444
445 =head1 NAME
446
447 B<uagen> - A Firefox User-Agent generator for Privoxy and Mozilla browsers
448
449 =head1 SYNOPSIS
450
451 B<uagen> [B<--action-file> I<action_file>] [B<--action-injection>]
452 [B<--browser-release-date> I<browser_release_date>]
453 [B<--browser-revision> I<browser_revision>]
454 [B<--browser-version> I<browser_version>]
455 [B<--clean-prefs-file>]
456 [B<--help>] [B<--language-overwrite> I<language(s)>]
457 [B<--logfile> I<logfile>] [B<--loop>] [B<--no-action-file>] [B<--no-logfile>]
458 [B<--prefs-file> I<prefs_file>] [B<--randomize-release-date>]
459 [B<--quiet>] [B<--sleeping-time> I<minutes>] [B<--silent>] [B<--version>]
460
461 =head1 DESCRIPTION
462
463 B<uagen> generates a fake Firefox User-Agent and writes it into a Privoxy action file
464 as parameter for Privoxy's B<hide-user-agent> action. Operating system, architecture,
465 platform, language and, optionally, the build date are randomized.
466
467 The generated language is also used as parameter for the
468 B<hide-accept-language> action which is understood by Privoxy since
469 version 3.0.5 beta.
470
471 Additionally the User-Agent can be written into prefs.js files which are
472 used by many Mozilla browsers.
473
474 =head1 OPTIONS
475
476 B<--action-file> I<action_file> Privoxy action file to write the
477 generated actions into. Default is /etc/privoxy/user-agent.action.
478
479 B<--action-injection> Don't generate a new action file from scratch,
480 but read an old one and just replace the action values. Useful
481 to keep custom URL patterns. For this to work, the action file
482 has to be already present. B<uagen> neither checks the syntax
483 nor cares if all actions are present. Garbage in, garbage out.
484
485 B<--browser-release-date> I<browser_release_date> Date to use.
486 The format is YYYYMMDD. Some sanity checks are done, but you
487 shouldn't rely on them.
488
489 B<--browser-revision> I<browser_revision> Use a custom revision.
490 B<uagen> will use it without any sanity checks.
491
492 B<--browser-version> I<browser_version> Use a custom browser version.
493 B<uagen> will use it without any sanity checks.
494
495 B<--clean-prefs-file> The I<prefs_file> is read and the variables
496 B<general.useragent.override> and B<intl.accept_languages> are removed.
497 Only effective if I<prefs_file> is set, and only useful if you want
498 to use the browser's defaults again.
499
500 B<--help> List command line options and exit.
501
502 B<--language-overwrite> I<language(s)> Comma separated list of language codes
503 to overwrite the default values. B<uagen> chooses one of them for the generated
504 User-Agent, by default the chosen language in lower cases is also used as
505 B<hide-accept-language> parameter.
506
507 B<--logfile> I<logfile> Logfile to save error messages and the generated
508 User-Agents. Default is /var/log/uagen.log.
509
510 B<--loop> Don't exit after the generation of the action file. Sleep for
511 a while and generate a new one instead. Useful if you don't have cron(8).
512
513 B<--no-logfile> Don't log anything.
514
515 B<--no-action-file> Don't write the action file.
516
517 B<--no-hide-accept-language> Stay compatible with Privoxy 3.0.3
518 and don't generate the B<hide-accept-language> action line. You should
519 really update your Privoxy version instead.
520
521 B<--prefs-file> I<prefs_file> Use the generated User-Agent to set the
522 B<general.useragent.override> variable in the Mozilla preference file
523 I<prefs_file>, The B<intl.accept_languages> variable will be set as well.
524
525 Firefox's preference file is usually located in
526 ~/.mozilla/firefox/*.default/prefs.js. Note that Firefox doesn't reread
527 the file once it is running.
528
529 B<--randomize-release-date> Randomly pick a date between the configured
530 release date and the actual date. Note that Firefox versions after 4.0
531 no longer provide the build date in the User-Agent header, so if you
532 randomize the date anyway, it will be obvious that the generated User-Agent
533 is fake.
534
535 B<--quiet> Don't print the generated User-Agent to the console.
536
537 B<--sleeping-time> I<minutes> Time to sleep. Only effective if used with B<--loop>.
538
539 B<--silent> Don't print the generated User-Agent to the console.
540
541 B<--version> Print version and exit.
542
543 The second dash is optional, options can be shortened, as long as there are
544 no ambiguities.
545
546 =head1 PRIVOXY CONFIGURATION
547
548 In Privoxy's configuration file the line:
549
550     actionsfile user-agent.action
551
552 should be added after:
553
554     actionfile default.action
555
556 and before:
557
558     actionfile user.action
559
560 This way the user can still use custom User-Agents
561 in I<user.action>. I<user-agent> has to be the name
562 of the generated action file.
563
564 If you are using Privoxy 3.0.6 or earlier, don't add the ".action" extension.
565
566 =head1 EXAMPLES
567
568 Without any options, B<uagen> creates an action file like:
569
570  {+hide-accept-language{en-ca} \
571   +hide-user-agent{Mozilla/5.0 (X11; U; OpenBSD i386; en-CA; rv:1.8.0.4) Gecko/20060628 Firefox/1.5.0.4} \
572  }
573  /
574
575 with the --no-accept-language option the generated file
576 could look like this one:
577
578  {+hide-user-agent{Mozilla/5.0 (X11; U; FreeBSD i386; de-DE; rv:1.8.0.4) Gecko/20060720 Firefox/1.5.0.4} \
579  }
580  /
581
582 =head1 CAVEATS
583
584 If the browser opens an encrypted connection, Privoxy can't inspect
585 the content and the browser's headers reach the server unmodified.
586 It is the user's job to use Privoxy's limit-connect action to make sure
587 there are no encrypted connections to untrusted sites.
588
589 Mozilla users can alter the browser's User-Agent with the
590 B<--prefs-file> option. But note that the preference file is only read
591 on startup. If the browser is already running, B<uagen's> changes will be ignored.
592
593 Hiding the User-Agent is pointless if the browser accepts all
594 cookies or even is configured for remote maintenance through Flash,
595 JavaScript, Java or similar security problems.
596
597 =head1 BUGS
598
599 Some parameters can't be specified at the command line.
600
601 =head1 SEE ALSO
602
603 privoxy(1)
604
605 =head1 AUTHOR
606
607 Fabian Keil <fk@fabiankeil.de>
608
609 http://www.fabiankeil.de/sourcecode/uagen/
610
611 http://www.fabiankeil.de/blog-surrogat/2006/01/26/firefox-user-agent-generator.html (German)
612
613 =cut
614