Accept and highlight: Compressed content from 29258 to 8630 bytes.
[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.10 2010/12/11 15:54:30 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    # These variables belong together. If you only change one of them, the generated
55    # User-Agent might be invalid. If you're not sure which values make sense,
56    # are too lazy to check, but want to change them anyway, take the values you
57    # see in the "Help/About Mozilla Firefox" menu.
58
59    BROWSER_VERSION                   => "3.6.15",
60    BROWSER_REVISION                  => '1.9.2.15',
61    BROWSER_RELEASE_DATE              => '20110305',
62 };
63
64 use constant LANGUAGES => qw(
65    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
66 );
67
68 #######################################################################################
69
70 sub generate_creation_time($) {
71     my $release_date = $_ = shift;
72
73     my ($rel_year, $rel_mon, $rel_day);
74     my ($c_day, $c_mon, $c_year);
75     my $now = time;
76     my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
77        localtime $now;
78     $mon  += 1;
79     $year += 1900;
80
81     unless ( m/\d{6}/ ) {
82         log_error("Invalid release date format: $release_date. Using "
83                   . BROWSER_RELEASE_DATE . " instead.");
84         $release_date = BROWSER_RELEASE_DATE;
85     }
86     $rel_year = substr $release_date, 0, 4;
87     $rel_mon  = substr $release_date, 4, 2;
88     $rel_day  = substr $release_date, 6, 2;
89
90     #1, 2, 3, Check.
91     die "release year in the future" if ( $year < $rel_year );
92     die "release month in the future"
93       if ( ( $year == $rel_year ) and ( $mon < $rel_mon ) );
94     die "release day in the future"
95       if (  ( $year == $rel_year )
96         and ( $mon  == $rel_mon )
97         and ( $mday  < $rel_day ) );
98
99     my @c_time = (0, 0, 0, $rel_day, $rel_mon - 1, $rel_year - 1900, 0, 0, 0);
100     my $c_seconds = &timelocal( @c_time );
101
102     $c_seconds = $now - (int rand ($now - $c_seconds));
103     @c_time = localtime $c_seconds;
104     ($sec, $min, $hour, $c_day, $c_mon, $c_year, $wday, $yday, $isdst) = @c_time;
105     $c_mon  += 1;
106     $c_year += 1900;
107
108     #3, 2, 1, Test.
109     die "Compilation year in the future" if ( $year < $c_year );
110     die "Compilation month in the future"
111       if ( ( $year == $c_year ) and ( $mon < $c_mon ) );
112     die "Compilation day in the future"
113       if ( ( $year == $c_year ) and ( $mon == $c_mon ) and ( $mday < $c_day ) );
114
115     return sprintf "%.2i%.2i%.2i", $c_year, $c_mon, $c_day;
116 }
117
118 sub generate_language_settings() {
119
120     our @languages;
121
122     my $language_i      = int rand (@languages);
123     my $accept_language = $languages[$language_i];
124     $accept_language =~ tr/[A-Z]/[a-z]/;
125
126     return ($languages[$language_i], $accept_language);
127 }
128
129 sub generate_platform_and_os() {
130
131     my %os_data = (
132         FreeBSD => {
133             karma             => 1,
134             platform          => 'X11',
135             architectures     => [ 'i386', 'amd64', 'sparc64', 'alpha' ],
136             order_is_inversed => 0,
137         },
138         OpenBSD => {
139             karma             => 1,
140             platform          => 'X11',
141             architectures     => [ 'i386', 'amd64', 'sparc64', 'alpha' ],
142             order_is_inversed => 0,
143         },
144         NetBSD => {
145             karma             => 1,
146             platform          => 'X11',
147             architectures     => [ 'i386', 'amd64', 'sparc64', 'alpha' ],
148             order_is_inversed => 0,
149         },
150         Linux => {
151             karma             => 1,
152             platform          => 'X11',
153             architectures     => [ 'i586', 'i686', 'x86_64' ],
154             order_is_inversed => 0,
155         },
156         SunOS => {
157             karma             => 1,
158             platform          => 'X11',
159             architectures     => [ 'i86pc', 'sun4u' ],
160             order_is_inversed => 0,
161         },
162         'Mac OS X' => {
163             karma             => 1,
164             platform          => 'Macintosh',
165             architectures     => [ 'PPC', 'Intel' ],
166             order_is_inversed => 1,
167         },
168         Windows => {
169             karma             => 0,
170             platform          => 'Windows',
171             architectures     => [ 'NT 5.1' ],
172             order_is_inversed => 0,
173         }
174     );
175
176     my @os_names;
177
178     foreach my $os_name ( keys %os_data ) {
179         push @os_names, ($os_name) x $os_data{$os_name}{'karma'}
180           if $os_data{$os_name}{'karma'};
181     }
182
183     my $os_i   = int rand(@os_names);
184     my $os     = $os_names[$os_i];
185     my $arch_i = int rand( @{ $os_data{$os}{'architectures'} } );
186     my $arch   = $os_data{$os}{'architectures'}[$arch_i];
187
188     my $platform = $os_data{$os}{'platform'};
189
190     my $os_or_cpu;
191     $os_or_cpu = sprintf "%s %s",
192       $os_data{$os}{'order_is_inversed'} ? ( $arch, $os ) : ( $os, $arch );
193
194     return $platform, $os_or_cpu;
195 }
196
197 sub generate_firefox_user_agent() {
198
199     our $languages;
200     our $browser_version;
201     our $browser_revision;
202     our $browser_release_date;
203
204     my $mozillaversion  = '5.0';
205     my $security        = "U";
206
207     my $creation_time = generate_creation_time($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; %s; %s; rv:%s) Gecko/%s Firefox/%s",
213       $mozillaversion, $platform, $security, $os_or_cpu, $locale, $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-2009 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     [--quiet]
364     [--silent]
365     [--sleeping-time $sleeping_time]
366     [--version]
367 see "perldoc $0" for more information
368     EOF
369     ;
370     exit(0);
371 }
372
373 sub main() {
374
375     my $error_message;
376     my  $no_action_file          = NO_ACTION_FILE;
377
378     our $silent                  = SILENT;
379     our $no_logging              = NO_LOGGING;
380     our $logfile                 = UAGEN_LOGFILE;
381     our $action_file             = ACTION_FILE;
382     our $browser_version         = BROWSER_VERSION;
383     our $browser_revision        = BROWSER_REVISION;
384     our $browser_release_date    = BROWSER_RELEASE_DATE;
385     our $sleeping_time           = SLEEPING_TIME;
386     our $loop                    = LOOP;
387     our $no_hide_accept_language = 0;
388     our $action_injection        = 0;
389
390     our @languages;
391     our ( $accept_language, $user_agent );
392     our $mozilla_prefs_file = MOZILLA_PREFS_FILE;
393     our $clean_prefs = 0;
394
395     GetOptions('logfile=s' => \$logfile,
396                'action-file=s' => \$action_file,
397                'language-overwrite=s@' => \@languages,
398                'silent|quiet' => \$silent,
399                'no-hide-accept-language' => \$no_hide_accept_language,
400                'no-logfile' => \$no_logging,
401                'no-action-file' => \$no_action_file,
402                'browser-version=s' => \$browser_version,
403                'browser-revision=s' => \$browser_revision,
404                'browser-release-date=s' => \$browser_release_date,
405                'action-injection' => \$action_injection,
406                'loop' => \$loop,
407                'sleeping-time' => \$sleeping_time,
408                'prefs-file=s' => \$mozilla_prefs_file,
409                'clean-prefs-file' => \$clean_prefs,
410                'help' => \&help,
411                'version' => sub {VersionMessage() && exit(0)}
412     ) or exit(0);
413
414     if (@languages) {
415         @languages = split(/,/,join(',',@languages));
416     } else {
417         @languages = LANGUAGES;
418     }
419
420     srand( time ^ ( $$ + ( $$ << 15 ) ) );
421
422     do {
423         $error_message='';
424         ( $accept_language, $user_agent ) = generate_firefox_user_agent();
425
426         print "$user_agent\n" unless $silent;
427
428         write_action_file() unless $no_action_file;
429         write_prefs_file() if $mozilla_prefs_file;
430
431         log_to_file "Generated User-Agent: $user_agent";
432
433     } while ($loop && sleep($sleeping_time * 60));
434 }
435
436 main();
437 exit(0);
438
439 =head1 NAME
440
441 B<uagen> - A Firefox User-Agent generator for Privoxy and Mozilla browsers
442
443 =head1 SYNOPSIS
444
445 B<uagen> [B<--action-file> I<action_file>] [B<--action-injection>]
446 [B<--browser-release-date> I<browser_release_date>]
447 [B<--browser-revision> I<browser_revision>]
448 [B<--browser-version> I<browser_version>]
449 [B<--clean-prefs-file>]
450 [B<--help>] [B<--language-overwrite> I<language(s)>]
451 [B<--logfile> I<logfile>] [B<--loop>] [B<--no-action-file>] [B<--no-logfile>]
452 [B<--prefs-file> I<prefs_file>]
453 [B<--quiet>] [B<--sleeping-time> I<minutes>] [B<--silent>] [B<--version>]
454
455 =head1 DESCRIPTION
456
457 B<uagen> generates a fake Firefox User-Agent and writes it into a Privoxy action file
458 as parameter for Privoxy's B<hide-user-agent> action. Operating system, architecture,
459 platform, language and build date are randomized.
460
461 The generated language is also used as parameter for the
462 B<hide-accept-language> action which is understood by Privoxy since
463 version 3.0.5 beta.
464
465 Additionally the User-Agent can be written into prefs.js files which are
466 used by many Mozilla browsers.
467
468 =head1 OPTIONS
469
470 B<--action-file> I<action_file> Privoxy action file to write the
471 generated actions into. Default is /etc/privoxy/user-agent.action.
472
473 B<--action-injection> Don't generate a new action file from scratch,
474 but read an old one and just replace the action values. Useful
475 to keep custom URL patterns. For this to work, the action file
476 has to be already present. B<uagen> neither checks the syntax
477 nor cares if all actions are present. Garbage in, garbage out.
478
479 B<--browser-release-date> I<browser_release_date> Date when the faked
480 browser version was first released, format is YYYYMMDD. B<uagen> will
481 pick a date between the release date and the actual date to use it as
482 build time. Some sanity checks are done, but you shouldn't rely on them.
483
484 B<--browser-revision> I<browser_revision> Use a custom revision.
485 B<uagen> will use it without any sanity checks.
486
487 B<--browser-version> I<browser_version> Use a custom browser version.
488 B<uagen> will use it without any sanity checks.
489
490 B<--clean-prefs-file> The I<prefs_file> is read and the variables
491 B<general.useragent.override> and B<intl.accept_languages> are removed.
492 Only effective if I<prefs_file> is set, and only useful if you want
493 to use the browser's defaults again.
494
495 B<--help> List command line options and exit.
496
497 B<--language-overwrite> I<language(s)> Comma separated list of language codes
498 to overwrite the default values. B<uagen> chooses one of them for the generated
499 User-Agent, by default the chosen language in lower cases is also used as
500 B<hide-accept-language> parameter.
501
502 B<--logfile> I<logfile> Logfile to save error messages and the generated
503 User-Agents. Default is /var/log/uagen.log.
504
505 B<--loop> Don't exit after the generation of the action file. Sleep for
506 a while and generate a new one instead. Useful if you don't have cron(8).
507
508 B<--no-logfile> Don't log anything.
509
510 B<--no-action-file> Don't write the action file.
511
512 B<--no-hide-accept-language> Stay compatible with Privoxy 3.0.3
513 and don't generate the B<hide-accept-language> action line. You should
514 really update your Privoxy version instead.
515
516 B<--prefs-file> I<prefs_file> Use the generated User-Agent to set the
517 B<general.useragent.override> variable in the Mozilla preference file
518 I<prefs_file>, The B<intl.accept_languages> variable will be set as well.
519
520 Firefox's preference file is usually located in
521 ~/.mozilla/firefox/*.default/prefs.js. Note that Firefox doesn't reread
522 the file once it is running.
523
524 B<--quiet> Don't print the generated User-Agent to the console.
525
526 B<--sleeping-time> I<minutes> Time to sleep. Only effective if used with B<--loop>.
527
528 B<--silent> Don't print the generated User-Agent to the console.
529
530 B<--version> Print version and exit.
531
532 The second dash is optional, options can be shortened, as long as there are
533 no ambiguities.
534
535 =head1 PRIVOXY CONFIGURATION
536
537 In Privoxy's configuration file the line:
538
539     actionsfile user-agent.action
540
541 should be added after:
542
543     actionfile default.action
544
545 and before:
546
547     actionfile user.action
548
549 This way the user can still use custom User-Agents
550 in I<user.action>. I<user-agent> has to be the name
551 of the generated action file.
552
553 If you are using Privoxy 3.0.6 or earlier, don't add the ".action" extension.
554
555 =head1 EXAMPLES
556
557 Without any options, B<uagen> creates an action file like:
558
559  {+hide-accept-language{en-ca} \
560   +hide-user-agent{Mozilla/5.0 (X11; U; OpenBSD i386; en-CA; rv:1.8.0.4) Gecko/20060628 Firefox/1.5.0.4} \
561  }
562  /
563
564 with the --no-accept-language option the generated file
565 could look like this one:
566
567  {+hide-user-agent{Mozilla/5.0 (X11; U; FreeBSD i386; de-DE; rv:1.8.0.4) Gecko/20060720 Firefox/1.5.0.4} \
568  }
569  /
570
571 =head1 CAVEATS
572
573 If the browser opens an encrypted connection, Privoxy can't inspect
574 the content and the browser's headers reach the server unmodified.
575 It is the user's job to use Privoxy's limit-connect action to make sure
576 there are no encrypted connections to untrusted sites.
577
578 Mozilla users can alter the browser's User-Agent with the
579 B<--prefs-file> option. But note that the preference file is only read
580 on startup. If the browser is already running, B<uagen's> changes will be ignored.
581
582 Hiding the User-Agent is pointless if the browser accepts all
583 cookies or even is configured for remote maintenance through Flash,
584 JavaScript, Java or similar security problems.
585
586 =head1 BUGS
587
588 Some parameters can't be specified at the command line.
589
590 =head1 SEE ALSO
591
592 privoxy(1)
593
594 =head1 AUTHOR
595
596 Fabian Keil <fk@fabiankeil.de>
597
598 http://www.fabiankeil.de/sourcecode/uagen/
599
600 http://www.fabiankeil.de/blog-surrogat/2006/01/26/firefox-user-agent-generator.html (German)
601
602 =cut
603