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