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