Add #205: Document how commit messages should look like
[privoxy.git] / tools / uagen.pl
1 #!/usr/bin/perl
2
3 ##############################################################################################
4 # uagen (https://www.fabiankeil.de/sourcecode/uagen/)
5 #
6 # Generates a pseudo-random Firefox user agent and writes it into a Privoxy action file
7 # and optionally into a Mozilla prefs file. For documentation see 'perldoc uagen(.pl)'.
8 #
9 # Examples (created with v1.2.2):
10 #
11 # Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
12 # Mozilla/5.0 (Macintosh; PPC Mac OS X; rv:78.0) Gecko/20100101 Firefox/78.0
13 # Mozilla/5.0 (X11; NetBSD i386; rv:78.0) Gecko/20100101 Firefox/78.0
14 # Mozilla/5.0 (X11; OpenBSD alpha; rv:78.0) Gecko/20100101 Firefox/78.0
15 # Mozilla/5.0 (X11; FreeBSD amd64; rv:78.0) Gecko/20100101 Firefox/78.0
16 # Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
17 # Mozilla/5.0 (X11; ElectroBSD amd64; rv:78.0) Gecko/20100101 Firefox/78.0
18 # Mozilla/5.0 (X11; FreeBSD i386; rv:78.0) Gecko/20100101 Firefox/78.0
19 #
20 # Copyright (c) 2006-2022 Fabian Keil <fk@fabiankeil.de>
21 #
22 # Permission to use, copy, modify, and distribute this software for any
23 # purpose with or without fee is hereby granted, provided that the above
24 # copyright notice and this permission notice appear in all copies.
25 #
26 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
27 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
28 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
29 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
30 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
31 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
32 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
33 ##############################################################################################
34
35 use strict;
36 use warnings;
37 use Time::Local;
38 use Getopt::Long;
39
40 use constant {
41
42    UAGEN_VERSION       => 'uagen 1.2.4',
43
44    UAGEN_LOGFILE       => '/var/log/uagen.log',
45    ACTION_FILE         => '/etc/privoxy/user-agent.action',
46    MOZILLA_PREFS_FILE  => '',
47    SILENT              =>  0,
48    NO_LOGGING          =>  0,
49    NO_ACTION_FILE      =>  0,
50    LOOP                =>  0,
51    SLEEPING_TIME       =>  5,
52
53    # As of Firefox 4, the "Gecko token" has been frozen
54    # https://hacks.mozilla.org/2010/09/final-user-agent-string-for-firefox-4/
55    RANDOMIZE_RELEASE_DATE => 0,
56
57    # These variables belong together. If you only change one of them, the generated
58    # User-Agent might be invalid. If you're not sure which values make sense,
59    # are too lazy to check, but want to change them anyway, take the values you
60    # see in the "Help/About Mozilla Firefox" menu.
61
62    BROWSER_VERSION                   => "102.0",
63    BROWSER_REVISION                  => '102.0',
64    BROWSER_RELEASE_DATE              => '20100101',
65 };
66
67 use constant LANGUAGES => qw(
68    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
69 );
70
71 #######################################################################################
72
73 sub generate_creation_time($) {
74     my $release_date = shift;
75
76     my ($rel_year, $rel_mon, $rel_day);
77     my ($c_day, $c_mon, $c_year);
78     my $now = time;
79     my (undef, undef, undef, $mday, $mon, $year, undef, undef, undef) = localtime($now);
80     $mon  += 1;
81     $year += 1900;
82
83     unless ($release_date =~ m/\d{6}/) {
84         log_error("Invalid release date format: $release_date. Using "
85                   . BROWSER_RELEASE_DATE . " instead.");
86         $release_date = BROWSER_RELEASE_DATE;
87     }
88     $rel_year = substr($release_date, 0, 4);
89     $rel_mon  = substr($release_date, 4, 2);
90     $rel_day  = substr($release_date, 6, 2);
91
92     #1, 2, 3, Check.
93     die "release year in the future" if ($year < $rel_year);
94     die "release month in the future"
95       if (($year == $rel_year) and ($mon < $rel_mon));
96     die "release day in the future"
97       if (($year == $rel_year) and ($mon == $rel_mon) 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     (undef, undef, undef, $c_day, $c_mon, $c_year, undef, undef, undef) = @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         ElectroBSD => {
133             karma             => 1,
134             platform          => 'X11',
135             architectures     => [ 'i386', 'amd64' ],
136             order_is_inversed => 0,
137         },
138         FreeBSD => {
139             karma             => 1,
140             platform          => 'X11',
141             architectures     => [ 'i386', 'amd64' ],
142             order_is_inversed => 0,
143         },
144         OpenBSD => {
145             karma             => 1,
146             platform          => 'X11',
147             architectures     => [ 'arm64', 'i386', 'amd64', 'sparc64', 'alpha' ],
148             order_is_inversed => 0,
149         },
150         NetBSD => {
151             karma             => 1,
152             platform          => 'X11',
153             architectures     => [ 'i386', 'amd64', 'sparc64', 'alpha' ],
154             order_is_inversed => 0,
155         },
156         Linux => {
157             karma             => 1,
158             platform          => 'X11',
159             architectures     => [ 'aarch64', 'i586', 'i686', 'x86_64' ],
160             order_is_inversed => 0,
161         },
162         SunOS => {
163             karma             => 1,
164             platform          => 'X11',
165             architectures     => [ 'i86pc', 'sun4u' ],
166             order_is_inversed => 0,
167         },
168         'Mac OS X' => {
169             karma             => 1,
170             platform          => 'Macintosh',
171             architectures     => [ 'PPC', 'Intel' ],
172             order_is_inversed => 1,
173         },
174         Windows => {
175             karma             => 0,
176             platform          => 'Windows',
177             architectures     => [ 'NT 5.1' ],
178             order_is_inversed => 0,
179         }
180     );
181
182     my @os_names;
183
184     foreach my $os_name ( keys %os_data ) {
185         push @os_names, ($os_name) x $os_data{$os_name}{'karma'}
186           if $os_data{$os_name}{'karma'};
187     }
188
189     my $os_i   = int rand(@os_names);
190     my $os     = $os_names[$os_i];
191     my $arch_i = int rand( @{ $os_data{$os}{'architectures'} } );
192     my $arch   = $os_data{$os}{'architectures'}[$arch_i];
193
194     my $platform = $os_data{$os}{'platform'};
195
196     my $os_or_cpu;
197     $os_or_cpu = sprintf "%s %s",
198       $os_data{$os}{'order_is_inversed'} ? ( $arch, $os ) : ( $os, $arch );
199
200     return $platform, $os_or_cpu;
201 }
202
203 sub generate_firefox_user_agent() {
204
205     our $languages;
206     our $browser_version;
207     our $browser_revision;
208     our $browser_release_date;
209     our $randomize_release_date;
210
211     my $mozillaversion  = '5.0';
212
213     my $creation_time = $randomize_release_date ?
214         generate_creation_time($browser_release_date) : $browser_release_date;
215     my ( $locale,   $accept_language ) = generate_language_settings();
216     my ( $platform, $os_or_cpu )       = generate_platform_and_os;
217
218     my $firefox_user_agent =
219       sprintf "Mozilla/%s (%s; %s; rv:%s) Gecko/%s Firefox/%s",
220       $mozillaversion, $platform, $os_or_cpu, $browser_revision,
221       $creation_time, $browser_version;
222
223     return $accept_language, $firefox_user_agent;
224 }
225
226 sub log_to_file($) {
227
228     my $message = shift;
229
230     our $logfile;
231     our $no_logging;
232
233     my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
234       localtime time;
235     $year += 1900;
236     $mon  += 1;
237     my $logtime = sprintf "%i/%.2i/%.2i %.2i:%.2i", $year, $mon, $mday, $hour,
238       $min;
239
240     return if $no_logging;
241
242     open(my $log_fd, ">>", $logfile) || die "Writing " . $logfile . " failed";
243     printf $log_fd UAGEN_VERSION . " ($logtime) $message\n";
244     close($log_fd);
245 }
246
247 sub log_error($) {
248
249     my $message = shift;
250
251     $message = "Error: $message";
252     log_to_file($message);
253     print "$message\n";
254
255     exit(1);
256 }
257
258 sub write_action_file() {
259
260     our $action_file;
261     our $user_agent;
262     our $accept_language;
263     our $no_hide_accept_language;
264     our $action_injection;
265
266     my $action_file_content = '';
267
268     if ($action_injection){
269         open(my $actionfile_fd, "<", $action_file)
270             or log_error "Reading action file $action_file failed!";
271         while (<$actionfile_fd>) {
272             s@(hide-accept-language\{).*?(\})@$1$accept_language$2@;
273             s@(hide-user-agent\{).*?(\})@$1$user_agent$2@;
274             $action_file_content .= $_;
275         }
276         close($actionfile_fd);
277     } else {
278         $action_file_content = "{";
279         $action_file_content .= sprintf "+hide-accept-language{%s} \\\n",
280             $accept_language unless $no_hide_accept_language;
281         $action_file_content .= sprintf " +hide-user-agent{%s} \\\n}\n/\n",
282             $user_agent;
283     }
284     open(my $actionfile_fd, ">", $action_file)
285       or log_error "Writing action file $action_file failed!";
286     print $actionfile_fd $action_file_content;
287     close($actionfile_fd);
288
289     return 0;
290 }
291
292 sub write_prefs_file() {
293
294     our $mozilla_prefs_file;
295     our $user_agent;
296     our $accept_language;
297     our $clean_prefs;
298
299     my $prefs_file_content = '';
300     my $prefsfile_fd;
301
302     if (open($prefsfile_fd, "<", $mozilla_prefs_file)) {
303
304         while (<$prefsfile_fd>) {
305             s@user_pref\(\"general.useragent.override\",.*\);\n?@@;
306             s@user_pref\(\"intl.accept_languages\",.*\);\n?@@;
307             $prefs_file_content .= $_;
308         }
309         close($prefsfile_fd);
310     } else {
311         log_error "Reading prefs file $mozilla_prefs_file failed. Creating a new file!";
312     }
313
314     $prefs_file_content .=
315         sprintf("user_pref(\"general.useragent.override\", \"%s\");\n", $user_agent) .
316         sprintf("user_pref(\"intl.accept_languages\", \"%s\");\n", $accept_language)
317         unless $clean_prefs;
318
319     open($prefsfile_fd, ">", $mozilla_prefs_file)
320       or log_error "Writing prefs file $mozilla_prefs_file failed!";
321     print $prefsfile_fd $prefs_file_content;
322     close($prefsfile_fd);
323
324 }
325
326 sub VersionMessage() {
327     printf UAGEN_VERSION . "\n" . 'Copyright (c) 2006-2022 Fabian Keil <fk@fabiankeil.de> ' .
328         "\nhttps://www.fabiankeil.de/sourcecode/uagen/\n";
329 }
330
331 sub help() {
332
333     our $logfile;
334     our $action_file;
335     our $browser_version;
336     our $browser_revision;
337     our $browser_release_date;
338     our $sleeping_time;
339     our $loop;
340     our $mozilla_prefs_file;
341
342     my $comma_separated_languages;
343
344     $loop = $loop ? ' ' . $loop : '';
345     $mozilla_prefs_file = $mozilla_prefs_file ? ' ' . $mozilla_prefs_file : '';
346     foreach (LANGUAGES){
347         $comma_separated_languages .= $_ . ",";
348     }
349     chop $comma_separated_languages;
350
351     VersionMessage;
352
353     print << "    EOF"
354
355 Options and their default values if there are any:
356     [--action-file $action_file]
357     [--action-injection]
358     [--browser-release-date $browser_release_date]
359     [--browser-revision $browser_revision]
360     [--browser-version $browser_version]
361     [--clean-prefs-file]
362     [--help]
363     [--language-overwrite $comma_separated_languages]
364     [--logfile $logfile]
365     [--loop$loop]
366     [--no-action-file]
367     [--no-hide-accept-language]
368     [--no-logfile]
369     [--prefs-file$mozilla_prefs_file]
370     [--randomize-release-date]
371     [--quiet]
372     [--silent]
373     [--sleeping-time $sleeping_time]
374     [--version]
375 see "perldoc $0" for more information
376     EOF
377     ;
378     exit(0);
379 }
380
381 sub main() {
382
383     my $error_message;
384     my  $no_action_file          = NO_ACTION_FILE;
385
386     our $silent                  = SILENT;
387     our $no_logging              = NO_LOGGING;
388     our $logfile                 = UAGEN_LOGFILE;
389     our $action_file             = ACTION_FILE;
390     our $randomize_release_date  = RANDOMIZE_RELEASE_DATE;
391     our $browser_version         = BROWSER_VERSION;
392     our $browser_revision        = BROWSER_REVISION;
393     our $browser_release_date    = BROWSER_RELEASE_DATE;
394     our $sleeping_time           = SLEEPING_TIME;
395     our $loop                    = LOOP;
396     our $no_hide_accept_language = 0;
397     our $action_injection        = 0;
398
399     our @languages;
400     our ( $accept_language, $user_agent );
401     our $mozilla_prefs_file = MOZILLA_PREFS_FILE;
402     our $clean_prefs = 0;
403
404     GetOptions('logfile=s' => \$logfile,
405                'action-file=s' => \$action_file,
406                'language-overwrite=s@' => \@languages,
407                'silent|quiet' => \$silent,
408                'no-hide-accept-language' => \$no_hide_accept_language,
409                'no-logfile' => \$no_logging,
410                'no-action-file' => \$no_action_file,
411                'randomize-release-date' => \$randomize_release_date,
412                'browser-version=s' => \$browser_version,
413                'browser-revision=s' => \$browser_revision,
414                'browser-release-date=s' => \$browser_release_date,
415                'action-injection' => \$action_injection,
416                'loop' => \$loop,
417                'sleeping-time' => \$sleeping_time,
418                'prefs-file=s' => \$mozilla_prefs_file,
419                'clean-prefs-file' => \$clean_prefs,
420                'help' => \&help,
421                'version' => sub {VersionMessage() && exit(0)}
422     ) or exit(0);
423
424     if (@languages) {
425         @languages = split(/,/,join(',',@languages));
426     } else {
427         @languages = LANGUAGES;
428     }
429
430     srand( time ^ ( $$ + ( $$ << 15 ) ) );
431
432     do {
433         $error_message='';
434         ( $accept_language, $user_agent ) = generate_firefox_user_agent();
435
436         print "$user_agent\n" unless $silent;
437
438         write_action_file() unless $no_action_file;
439         write_prefs_file() if $mozilla_prefs_file;
440
441         log_to_file "Generated User-Agent: $user_agent";
442
443     } while ($loop && sleep($sleeping_time * 60));
444 }
445
446 main();
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. Note that the Mozilla project has frozen
491 the "Gecko token" starting with Firefox 4 so using a different
492 one than the default is somewhat suspicious.
493
494 B<--browser-revision> I<browser_revision> Use a custom revision.
495 B<uagen> will use it without any sanity checks.
496
497 B<--browser-version> I<browser_version> Use a custom browser version.
498 B<uagen> will use it without any sanity checks.
499
500 B<--clean-prefs-file> The I<prefs_file> is read and the variables
501 B<general.useragent.override> and B<intl.accept_languages> are removed.
502 Only effective if I<prefs_file> is set, and only useful if you want
503 to use the browser's defaults again.
504
505 B<--help> List command line options and exit.
506
507 B<--language-overwrite> I<language(s)> Comma separated list of language codes
508 to overwrite the default values. B<uagen> chooses one of them for the generated
509 User-Agent, by default the chosen language in lower cases is also used as
510 B<hide-accept-language> parameter.
511
512 B<--logfile> I<logfile> Logfile to save error messages and the generated
513 User-Agents. Default is /var/log/uagen.log.
514
515 B<--loop> Don't exit after the generation of the action file. Sleep for
516 a while and generate a new one instead. Useful if you don't have cron(8).
517
518 B<--no-logfile> Don't log anything.
519
520 B<--no-action-file> Don't write the action file.
521
522 B<--no-hide-accept-language> Stay compatible with Privoxy 3.0.3
523 and don't generate the B<hide-accept-language> action line. You should
524 really update your Privoxy version instead.
525
526 B<--prefs-file> I<prefs_file> Use the generated User-Agent to set the
527 B<general.useragent.override> variable in the Mozilla preference file
528 I<prefs_file>, The B<intl.accept_languages> variable will be set as well.
529
530 Firefox's preference file is usually located in
531 ~/.mozilla/firefox/*.default/prefs.js. Note that Firefox doesn't reread
532 the file once it is running.
533
534 B<--randomize-release-date> Randomly pick a date between the configured
535 release date and the actual date. Note that Firefox versions after 4.0
536 no longer provide the build date in the User-Agent header, so if you
537 randomize the date anyway, it will be obvious that the generated User-Agent
538 is fake.
539
540 B<--quiet> Don't print the generated User-Agent to the console.
541
542 B<--sleeping-time> I<minutes> Time to sleep. Only effective if used with B<--loop>.
543
544 B<--silent> Don't print the generated User-Agent to the console.
545
546 B<--version> Print version and exit.
547
548 The second dash is optional, options can be shortened, as long as there are
549 no ambiguities.
550
551 =head1 PRIVOXY CONFIGURATION
552
553 In Privoxy's configuration file the line:
554
555     actionsfile user-agent.action
556
557 should be added after:
558
559     actionfile default.action
560
561 and before:
562
563     actionfile user.action
564
565 This way the user can still use custom User-Agents
566 in I<user.action>. I<user-agent> has to be the name
567 of the generated action file.
568
569 If you are using Privoxy 3.0.6 or earlier, don't add the ".action" extension.
570
571 =head1 EXAMPLES
572
573 Without any options, B<uagen> creates an action file like:
574
575  {+hide-accept-language{en-ca} \
576   +hide-user-agent{Mozilla/5.0 (X11; U; OpenBSD i386; en-CA; rv:1.8.0.4) Gecko/20060628 Firefox/1.5.0.4} \
577  }
578  /
579
580 with the --no-accept-language option the generated file
581 could look like this one:
582
583  {+hide-user-agent{Mozilla/5.0 (X11; U; FreeBSD i386; de-DE; rv:1.8.0.4) Gecko/20060720 Firefox/1.5.0.4} \
584  }
585  /
586
587 =head1 CAVEATS
588
589 Use the https-inspection action to make sure Privoxy can modify
590 the browser's headers for encrypted traffic as well.
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(8)
607
608 =head1 AUTHOR
609
610 Fabian Keil <fk@fabiankeil.de>
611
612 https://www.fabiankeil.de/sourcecode/uagen/
613
614 https://www.fabiankeil.de/blog-surrogat/2006/01/26/firefox-user-agent-generator.html (German)
615
616 =cut
617