Rename the tidy target to clean-editor-files so it can't be confused with the dok...
[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.16 2011/06/29 20:29:58 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
444 =head1 NAME
445
446 B<uagen> - A Firefox User-Agent generator for Privoxy and Mozilla browsers
447
448 =head1 SYNOPSIS
449
450 B<uagen> [B<--action-file> I<action_file>] [B<--action-injection>]
451 [B<--browser-release-date> I<browser_release_date>]
452 [B<--browser-revision> I<browser_revision>]
453 [B<--browser-version> I<browser_version>]
454 [B<--clean-prefs-file>]
455 [B<--help>] [B<--language-overwrite> I<language(s)>]
456 [B<--logfile> I<logfile>] [B<--loop>] [B<--no-action-file>] [B<--no-logfile>]
457 [B<--prefs-file> I<prefs_file>] [B<--randomize-release-date>]
458 [B<--quiet>] [B<--sleeping-time> I<minutes>] [B<--silent>] [B<--version>]
459
460 =head1 DESCRIPTION
461
462 B<uagen> generates a fake Firefox User-Agent and writes it into a Privoxy action file
463 as parameter for Privoxy's B<hide-user-agent> action. Operating system, architecture,
464 platform, language and, optionally, the build date are randomized.
465
466 The generated language is also used as parameter for the
467 B<hide-accept-language> action which is understood by Privoxy since
468 version 3.0.5 beta.
469
470 Additionally the User-Agent can be written into prefs.js files which are
471 used by many Mozilla browsers.
472
473 =head1 OPTIONS
474
475 B<--action-file> I<action_file> Privoxy action file to write the
476 generated actions into. Default is /etc/privoxy/user-agent.action.
477
478 B<--action-injection> Don't generate a new action file from scratch,
479 but read an old one and just replace the action values. Useful
480 to keep custom URL patterns. For this to work, the action file
481 has to be already present. B<uagen> neither checks the syntax
482 nor cares if all actions are present. Garbage in, garbage out.
483
484 B<--browser-release-date> I<browser_release_date> Date to use.
485 The format is YYYYMMDD. Some sanity checks are done, but you
486 shouldn't rely on them.
487
488 B<--browser-revision> I<browser_revision> Use a custom revision.
489 B<uagen> will use it without any sanity checks.
490
491 B<--browser-version> I<browser_version> Use a custom browser version.
492 B<uagen> will use it without any sanity checks.
493
494 B<--clean-prefs-file> The I<prefs_file> is read and the variables
495 B<general.useragent.override> and B<intl.accept_languages> are removed.
496 Only effective if I<prefs_file> is set, and only useful if you want
497 to use the browser's defaults again.
498
499 B<--help> List command line options and exit.
500
501 B<--language-overwrite> I<language(s)> Comma separated list of language codes
502 to overwrite the default values. B<uagen> chooses one of them for the generated
503 User-Agent, by default the chosen language in lower cases is also used as
504 B<hide-accept-language> parameter.
505
506 B<--logfile> I<logfile> Logfile to save error messages and the generated
507 User-Agents. Default is /var/log/uagen.log.
508
509 B<--loop> Don't exit after the generation of the action file. Sleep for
510 a while and generate a new one instead. Useful if you don't have cron(8).
511
512 B<--no-logfile> Don't log anything.
513
514 B<--no-action-file> Don't write the action file.
515
516 B<--no-hide-accept-language> Stay compatible with Privoxy 3.0.3
517 and don't generate the B<hide-accept-language> action line. You should
518 really update your Privoxy version instead.
519
520 B<--prefs-file> I<prefs_file> Use the generated User-Agent to set the
521 B<general.useragent.override> variable in the Mozilla preference file
522 I<prefs_file>, The B<intl.accept_languages> variable will be set as well.
523
524 Firefox's preference file is usually located in
525 ~/.mozilla/firefox/*.default/prefs.js. Note that Firefox doesn't reread
526 the file once it is running.
527
528 B<--randomize-release-date> Randomly pick a date between the configured
529 release date and the actual date. Note that Firefox versions after 4.0
530 no longer provide the build date in the User-Agent header, so if you
531 randomize the date anyway, it will be obvious that the generated User-Agent
532 is fake.
533
534 B<--quiet> Don't print the generated User-Agent to the console.
535
536 B<--sleeping-time> I<minutes> Time to sleep. Only effective if used with B<--loop>.
537
538 B<--silent> Don't print the generated User-Agent to the console.
539
540 B<--version> Print version and exit.
541
542 The second dash is optional, options can be shortened, as long as there are
543 no ambiguities.
544
545 =head1 PRIVOXY CONFIGURATION
546
547 In Privoxy's configuration file the line:
548
549     actionsfile user-agent.action
550
551 should be added after:
552
553     actionfile default.action
554
555 and before:
556
557     actionfile user.action
558
559 This way the user can still use custom User-Agents
560 in I<user.action>. I<user-agent> has to be the name
561 of the generated action file.
562
563 If you are using Privoxy 3.0.6 or earlier, don't add the ".action" extension.
564
565 =head1 EXAMPLES
566
567 Without any options, B<uagen> creates an action file like:
568
569  {+hide-accept-language{en-ca} \
570   +hide-user-agent{Mozilla/5.0 (X11; U; OpenBSD i386; en-CA; rv:1.8.0.4) Gecko/20060628 Firefox/1.5.0.4} \
571  }
572  /
573
574 with the --no-accept-language option the generated file
575 could look like this one:
576
577  {+hide-user-agent{Mozilla/5.0 (X11; U; FreeBSD i386; de-DE; rv:1.8.0.4) Gecko/20060720 Firefox/1.5.0.4} \
578  }
579  /
580
581 =head1 CAVEATS
582
583 If the browser opens an encrypted connection, Privoxy can't inspect
584 the content and the browser's headers reach the server unmodified.
585 It is the user's job to use Privoxy's limit-connect action to make sure
586 there are no encrypted connections to untrusted sites.
587
588 Mozilla users can alter the browser's User-Agent with the
589 B<--prefs-file> option. But note that the preference file is only read
590 on startup. If the browser is already running, B<uagen's> changes will be ignored.
591
592 Hiding the User-Agent is pointless if the browser accepts all
593 cookies or even is configured for remote maintenance through Flash,
594 JavaScript, Java or similar security problems.
595
596 =head1 BUGS
597
598 Some parameters can't be specified at the command line.
599
600 =head1 SEE ALSO
601
602 privoxy(1)
603
604 =head1 AUTHOR
605
606 Fabian Keil <fk@fabiankeil.de>
607
608 http://www.fabiankeil.de/sourcecode/uagen/
609
610 http://www.fabiankeil.de/blog-surrogat/2006/01/26/firefox-user-agent-generator.html (German)
611
612 =cut
613