66e2c578c7517ad5f54383db712a889c09eaaca5
[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.21 2012/05/24 15:00:27 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.2.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                   => "14.0.1",
64    BROWSER_REVISION                  => '14.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
208     my $creation_time = $randomize_release_date ?
209         generate_creation_time($browser_release_date) : $browser_release_date;
210     my ( $locale,   $accept_language ) = generate_language_settings();
211     my ( $platform, $os_or_cpu )       = generate_platform_and_os;
212
213     my $firefox_user_agent =
214       sprintf "Mozilla/%s (%s; %s; rv:%s) Gecko/%s Firefox/%s",
215       $mozillaversion, $platform, $os_or_cpu, $browser_revision,
216       $creation_time, $browser_version;
217
218     return $accept_language, $firefox_user_agent;
219 }
220
221 sub log_to_file($) {
222
223     my $message = shift;
224
225     our $logfile;
226     our $no_logging;
227
228     my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
229       localtime time;
230     $year += 1900;
231     $mon  += 1;
232     my $logtime = sprintf "%i/%.2i/%.2i %.2i:%.2i", $year, $mon, $mday, $hour,
233       $min;
234
235     return if $no_logging;
236
237     open(my $log_fd, ">>", $logfile) || die "Writing " . $logfile . " failed";
238     printf $log_fd UAGEN_VERSION . " ($logtime) $message\n";
239     close($log_fd);
240 }
241
242 sub log_error($) {
243
244     my $message = shift;
245
246     $message = "Error: $message";
247     log_to_file($message);
248     print "$message\n";
249
250     exit(1);
251 }
252
253 sub write_action_file() {
254
255     our $action_file;
256     our $user_agent;
257     our $accept_language;
258     our $no_hide_accept_language;
259     our $action_injection;
260
261     my $action_file_content = '';
262
263     if ($action_injection){
264         open(my $actionfile_fd, "<", $action_file)
265             or log_error "Reading action file $action_file failed!";
266         while (<$actionfile_fd>) {
267             s@(hide-accept-language\{).*?(\})@$1$accept_language$2@;
268             s@(hide-user-agent\{).*?(\})@$1$user_agent$2@;
269             $action_file_content .= $_;
270         }
271         close($actionfile_fd);
272     } else {
273         $action_file_content = "{";
274         $action_file_content .= sprintf "+hide-accept-language{%s} \\\n",
275             $accept_language unless $no_hide_accept_language;
276         $action_file_content .= sprintf " +hide-user-agent{%s} \\\n}\n/\n",
277             $user_agent;
278     }
279     open(my $actionfile_fd, ">", $action_file)
280       or log_error "Writing action file $action_file failed!";
281     print $actionfile_fd $action_file_content;
282     close($actionfile_fd);
283
284     return 0;
285 }
286
287 sub write_prefs_file() {
288
289     our $mozilla_prefs_file;
290     our $user_agent;
291     our $accept_language;
292     our $clean_prefs;
293
294     my $prefs_file_content = '';
295     my $prefsfile_fd;
296
297     if (open($prefsfile_fd, "<", $mozilla_prefs_file)) {
298
299         while (<$prefsfile_fd>) {
300             s@user_pref\(\"general.useragent.override\",.*\);\n?@@;
301             s@user_pref\(\"intl.accept_languages\",.*\);\n?@@;
302             $prefs_file_content .= $_;
303         }
304         close($prefsfile_fd);
305     } else {
306         log_error "Reading prefs file $mozilla_prefs_file failed. Creating a new file!";
307     }
308
309     $prefs_file_content .=
310         sprintf("user_pref(\"general.useragent.override\", \"%s\");\n", $user_agent) .
311         sprintf("user_pref(\"intl.accept_languages\", \"%s\");\n", $accept_language)
312         unless $clean_prefs;
313
314     open($prefsfile_fd, ">", $mozilla_prefs_file)
315       or log_error "Writing prefs file $mozilla_prefs_file failed!";
316     print $prefsfile_fd $prefs_file_content;
317     close($prefsfile_fd);
318
319 }
320
321 sub VersionMessage() {
322     printf UAGEN_VERSION . "\n" . 'Copyright (C) 2006-2011 Fabian Keil <fk@fabiankeil.de> ' .
323         "\nhttp://www.fabiankeil.de/sourcecode/uagen/\n";
324 }
325
326 sub help() {
327
328     our $logfile;
329     our $action_file;
330     our $browser_version;
331     our $browser_revision;
332     our $browser_release_date;
333     our $sleeping_time;
334     our $loop;
335     our $mozilla_prefs_file;
336
337     my $comma_separated_languages;
338
339     $loop = $loop ? ' ' . $loop : '';
340     $mozilla_prefs_file = $mozilla_prefs_file ? ' ' . $mozilla_prefs_file : '';
341     foreach (LANGUAGES){
342         $comma_separated_languages .= $_ . ",";
343     }
344     chop $comma_separated_languages;
345
346     VersionMessage;
347
348     print << "    EOF"
349
350 Options and their default values if there are any:
351     [--action-file $action_file]
352     [--action-injection]
353     [--browser-release-date $browser_release_date]
354     [--browser-revision $browser_revision]
355     [--browser-version $browser_version]
356     [--clean-prefs-file]
357     [--help]
358     [--language-overwrite $comma_separated_languages]
359     [--logfile $logfile]
360     [--loop$loop]
361     [--no-action-file]
362     [--no-hide-accept-language]
363     [--no-logfile]
364     [--prefs-file$mozilla_prefs_file]
365     [--randomize-release-date]
366     [--quiet]
367     [--silent]
368     [--sleeping-time $sleeping_time]
369     [--version]
370 see "perldoc $0" for more information
371     EOF
372     ;
373     exit(0);
374 }
375
376 sub main() {
377
378     my $error_message;
379     my  $no_action_file          = NO_ACTION_FILE;
380
381     our $silent                  = SILENT;
382     our $no_logging              = NO_LOGGING;
383     our $logfile                 = UAGEN_LOGFILE;
384     our $action_file             = ACTION_FILE;
385     our $randomize_release_date  = RANDOMIZE_RELEASE_DATE;
386     our $browser_version         = BROWSER_VERSION;
387     our $browser_revision        = BROWSER_REVISION;
388     our $browser_release_date    = BROWSER_RELEASE_DATE;
389     our $sleeping_time           = SLEEPING_TIME;
390     our $loop                    = LOOP;
391     our $no_hide_accept_language = 0;
392     our $action_injection        = 0;
393
394     our @languages;
395     our ( $accept_language, $user_agent );
396     our $mozilla_prefs_file = MOZILLA_PREFS_FILE;
397     our $clean_prefs = 0;
398
399     GetOptions('logfile=s' => \$logfile,
400                'action-file=s' => \$action_file,
401                'language-overwrite=s@' => \@languages,
402                'silent|quiet' => \$silent,
403                'no-hide-accept-language' => \$no_hide_accept_language,
404                'no-logfile' => \$no_logging,
405                'no-action-file' => \$no_action_file,
406                'randomize-release-date' => \$randomize_release_date,
407                'browser-version=s' => \$browser_version,
408                'browser-revision=s' => \$browser_revision,
409                'browser-release-date=s' => \$browser_release_date,
410                'action-injection' => \$action_injection,
411                'loop' => \$loop,
412                'sleeping-time' => \$sleeping_time,
413                'prefs-file=s' => \$mozilla_prefs_file,
414                'clean-prefs-file' => \$clean_prefs,
415                'help' => \&help,
416                'version' => sub {VersionMessage() && exit(0)}
417     ) or exit(0);
418
419     if (@languages) {
420         @languages = split(/,/,join(',',@languages));
421     } else {
422         @languages = LANGUAGES;
423     }
424
425     srand( time ^ ( $$ + ( $$ << 15 ) ) );
426
427     do {
428         $error_message='';
429         ( $accept_language, $user_agent ) = generate_firefox_user_agent();
430
431         print "$user_agent\n" unless $silent;
432
433         write_action_file() unless $no_action_file;
434         write_prefs_file() if $mozilla_prefs_file;
435
436         log_to_file "Generated User-Agent: $user_agent";
437
438     } while ($loop && sleep($sleeping_time * 60));
439 }
440
441 main();
442
443 =head1 NAME
444
445 B<uagen> - A Firefox User-Agent generator for Privoxy and Mozilla browsers
446
447 =head1 SYNOPSIS
448
449 B<uagen> [B<--action-file> I<action_file>] [B<--action-injection>]
450 [B<--browser-release-date> I<browser_release_date>]
451 [B<--browser-revision> I<browser_revision>]
452 [B<--browser-version> I<browser_version>]
453 [B<--clean-prefs-file>]
454 [B<--help>] [B<--language-overwrite> I<language(s)>]
455 [B<--logfile> I<logfile>] [B<--loop>] [B<--no-action-file>] [B<--no-logfile>]
456 [B<--prefs-file> I<prefs_file>] [B<--randomize-release-date>]
457 [B<--quiet>] [B<--sleeping-time> I<minutes>] [B<--silent>] [B<--version>]
458
459 =head1 DESCRIPTION
460
461 B<uagen> generates a fake Firefox User-Agent and writes it into a Privoxy action file
462 as parameter for Privoxy's B<hide-user-agent> action. Operating system, architecture,
463 platform, language and, optionally, the build date are randomized.
464
465 The generated language is also used as parameter for the
466 B<hide-accept-language> action which is understood by Privoxy since
467 version 3.0.5 beta.
468
469 Additionally the User-Agent can be written into prefs.js files which are
470 used by many Mozilla browsers.
471
472 =head1 OPTIONS
473
474 B<--action-file> I<action_file> Privoxy action file to write the
475 generated actions into. Default is /etc/privoxy/user-agent.action.
476
477 B<--action-injection> Don't generate a new action file from scratch,
478 but read an old one and just replace the action values. Useful
479 to keep custom URL patterns. For this to work, the action file
480 has to be already present. B<uagen> neither checks the syntax
481 nor cares if all actions are present. Garbage in, garbage out.
482
483 B<--browser-release-date> I<browser_release_date> Date to use.
484 The format is YYYYMMDD. Some sanity checks are done, but you
485 shouldn't rely on them.
486
487 B<--browser-revision> I<browser_revision> Use a custom revision.
488 B<uagen> will use it without any sanity checks.
489
490 B<--browser-version> I<browser_version> Use a custom browser version.
491 B<uagen> will use it without any sanity checks.
492
493 B<--clean-prefs-file> The I<prefs_file> is read and the variables
494 B<general.useragent.override> and B<intl.accept_languages> are removed.
495 Only effective if I<prefs_file> is set, and only useful if you want
496 to use the browser's defaults again.
497
498 B<--help> List command line options and exit.
499
500 B<--language-overwrite> I<language(s)> Comma separated list of language codes
501 to overwrite the default values. B<uagen> chooses one of them for the generated
502 User-Agent, by default the chosen language in lower cases is also used as
503 B<hide-accept-language> parameter.
504
505 B<--logfile> I<logfile> Logfile to save error messages and the generated
506 User-Agents. Default is /var/log/uagen.log.
507
508 B<--loop> Don't exit after the generation of the action file. Sleep for
509 a while and generate a new one instead. Useful if you don't have cron(8).
510
511 B<--no-logfile> Don't log anything.
512
513 B<--no-action-file> Don't write the action file.
514
515 B<--no-hide-accept-language> Stay compatible with Privoxy 3.0.3
516 and don't generate the B<hide-accept-language> action line. You should
517 really update your Privoxy version instead.
518
519 B<--prefs-file> I<prefs_file> Use the generated User-Agent to set the
520 B<general.useragent.override> variable in the Mozilla preference file
521 I<prefs_file>, The B<intl.accept_languages> variable will be set as well.
522
523 Firefox's preference file is usually located in
524 ~/.mozilla/firefox/*.default/prefs.js. Note that Firefox doesn't reread
525 the file once it is running.
526
527 B<--randomize-release-date> Randomly pick a date between the configured
528 release date and the actual date. Note that Firefox versions after 4.0
529 no longer provide the build date in the User-Agent header, so if you
530 randomize the date anyway, it will be obvious that the generated User-Agent
531 is fake.
532
533 B<--quiet> Don't print the generated User-Agent to the console.
534
535 B<--sleeping-time> I<minutes> Time to sleep. Only effective if used with B<--loop>.
536
537 B<--silent> Don't print the generated User-Agent to the console.
538
539 B<--version> Print version and exit.
540
541 The second dash is optional, options can be shortened, as long as there are
542 no ambiguities.
543
544 =head1 PRIVOXY CONFIGURATION
545
546 In Privoxy's configuration file the line:
547
548     actionsfile user-agent.action
549
550 should be added after:
551
552     actionfile default.action
553
554 and before:
555
556     actionfile user.action
557
558 This way the user can still use custom User-Agents
559 in I<user.action>. I<user-agent> has to be the name
560 of the generated action file.
561
562 If you are using Privoxy 3.0.6 or earlier, don't add the ".action" extension.
563
564 =head1 EXAMPLES
565
566 Without any options, B<uagen> creates an action file like:
567
568  {+hide-accept-language{en-ca} \
569   +hide-user-agent{Mozilla/5.0 (X11; U; OpenBSD i386; en-CA; rv:1.8.0.4) Gecko/20060628 Firefox/1.5.0.4} \
570  }
571  /
572
573 with the --no-accept-language option the generated file
574 could look like this one:
575
576  {+hide-user-agent{Mozilla/5.0 (X11; U; FreeBSD i386; de-DE; rv:1.8.0.4) Gecko/20060720 Firefox/1.5.0.4} \
577  }
578  /
579
580 =head1 CAVEATS
581
582 If the browser opens an encrypted connection, Privoxy can't inspect
583 the content and the browser's headers reach the server unmodified.
584 It is the user's job to use Privoxy's limit-connect action to make sure
585 there are no encrypted connections to untrusted sites.
586
587 Mozilla users can alter the browser's User-Agent with the
588 B<--prefs-file> option. But note that the preference file is only read
589 on startup. If the browser is already running, B<uagen's> changes will be ignored.
590
591 Hiding the User-Agent is pointless if the browser accepts all
592 cookies or even is configured for remote maintenance through Flash,
593 JavaScript, Java or similar security problems.
594
595 =head1 BUGS
596
597 Some parameters can't be specified at the command line.
598
599 =head1 SEE ALSO
600
601 privoxy(1)
602
603 =head1 AUTHOR
604
605 Fabian Keil <fk@fabiankeil.de>
606
607 http://www.fabiankeil.de/sourcecode/uagen/
608
609 http://www.fabiankeil.de/blog-surrogat/2006/01/26/firefox-user-agent-generator.html (German)
610
611 =cut
612