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