~ubuntu-branches/ubuntu/utopic/perspectives-extension/utopic

« back to all changes in this revision

Viewing changes to mexumgen.pl

  • Committer: Bazaar Package Importer
  • Author(s): Vagrant Cascadian
  • Date: 2011-07-01 18:18:11 UTC
  • Revision ID: james.westby@ubuntu.com-20110701181811-ryritrw5t76hrb9e
Tags: upstream-4.1.1~20110418
ImportĀ upstreamĀ versionĀ 4.1.1~20110418

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/perl
 
2
#Ethan Jackosn found this at http://www.softlights.net/projects/mexumgen/
 
3
#*******************************************************************************
 
4
#   Mozilla Extension Update Manifest Generator, version 1.1
 
5
#   Copyright (C) 2008 Sergei Zhirikov (sfzhi@yahoo.com)
 
6
#   This software is available under the GNU General Public License v3.0
 
7
#       (http://www.gnu.org/licenses/gpl-3.0.txt)
 
8
#*******************************************************************************
 
9
use strict;
 
10
use warnings;
 
11
use Pod::Usage;
 
12
use Getopt::Std;
 
13
use MIME::Base64;
 
14
use Convert::ASN1;
 
15
use RDF::Core::Parser;
 
16
use File::Spec::Functions qw(catfile tmpdir curdir);
 
17
#*******************************************************************************
 
18
use constant NSMOZ => 'http://www.mozilla.org/2004/em-rdf#';
 
19
use constant NSRDF => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
 
20
use constant sha512WithRSAEncryption => ':1.2.840.113549.1.1.13';
 
21
#*******************************************************************************
 
22
@ARGV or pod2usage(-exitval => 1, -verbose => 99, -sections => 'NAME|SYNOPSIS');
 
23
#*******************************************************************************
 
24
$Getopt::Std::STANDARD_HELP_VERSION = 1;
 
25
sub VERSION_MESSAGE {
 
26
    pod2usage(-exitval => 'NOEXIT', -verbose => 99, -sections => 'NAME');
 
27
}
 
28
sub HELP_MESSAGE {
 
29
    pod2usage(-exitval => 'NOEXIT', -verbose => 1);
 
30
}
 
31
#*******************************************************************************
 
32
our %opt;
 
33
getopts('i:o:k:p:hw', \%opt) or die "Use '--help' to see available options\n";
 
34
our ($rdf, $out, $pem, $pwd) = @opt{qw[i o k p]};
 
35
our $grp = $opt{w}? 3: 2;
 
36
#*******************************************************************************
 
37
@ARGV % $grp == 0 or die "The number of arguments must be multiple of $grp\n";
 
38
our (@xpi, %xpi);
 
39
while (@ARGV > 0) {
 
40
    my ($xpi, $url, $inf) = splice(@ARGV, 0, $grp);
 
41
    push(@xpi, {xpi => $xpi, url => $url, $inf? (inf => $inf): ()});
 
42
    $xpi{$xpi} = $#xpi;
 
43
}
 
44
$rdf || @xpi or die "At least one input file is required\n";
 
45
#*******************************************************************************
 
46
our $tmp = catfile(tmpdir() || curdir(), "update.rdf.mexumgen.$$.tmp");
 
47
#*******************************************************************************
 
48
sub rdf($) {
 
49
    my $tree = {};
 
50
    (new RDF::Core::Parser(Assert => sub {
 
51
        my %item = @_;
 
52
        push(@{$tree->{$item{'subject_uri'}}->
 
53
            {$item{'predicate_ns'}}->{$item{'predicate_name'}}},
 
54
            {uri => $item{'object_uri'}, lit => $item{'object_literal'}});
 
55
    }))->parse(shift);
 
56
    return $tree;
 
57
}
 
58
#*******************************************************************************
 
59
for my $xpi (@xpi) {
 
60
    my $txt = qx[unzip -jnpq "$xpi->{xpi}" install.rdf];
 
61
    $? == 0 or die "Could not extract install manifest from '$xpi->{xpi}'\n";
 
62
    my $rdf = rdf($txt);
 
63
    my $all = $rdf->{'urn:mozilla:install-manifest'}->{NSMOZ()};
 
64
    my ($ext, $ver) = map {
 
65
        (defined($_) && (@{$_} == 1))? $_->[0]->{lit}: undef;
 
66
    } @$all{'id', 'version'};
 
67
    my @app = map {
 
68
        my $uri = $_->{uri};
 
69
        (defined($uri) && exists($rdf->{$uri}))? (sub {
 
70
            my ($app, $min, $max) = map {
 
71
                (defined($_) && (@{$_} == 1))? $_->[0]->{lit}: undef;
 
72
            } @{$rdf->{$uri}->{NSMOZ()}}{'id', 'minVersion', 'maxVersion'};
 
73
            {app => $app, min => $min, max => $max};
 
74
        })->(): ();
 
75
    } @{$all->{'targetApplication'}};
 
76
    $xpi->{ext} = $ext;
 
77
    $xpi->{ver} = $ver;
 
78
    $xpi->{app} = \@app;
 
79
}
 
80
#*******************************************************************************
 
81
if (@xpi && ($pem || $opt{h})) {
 
82
    open(SHA, '-|', 'openssl sha1 -hex '.join(' ', map {qq["$_->{xpi}"]} @xpi))
 
83
        or die "Failed to start OpenSSL to calculate SHA1 hashes: $!\n";
 
84
    while(<SHA>) {
 
85
        if (/^SHA1\((.*?)\)=\s*([[:xdigit:]]{40})\s*$/) {
 
86
            $xpi[$xpi{$1}]->{sha} = "sha1:$2" if (exists($xpi{$1}));
 
87
        }
 
88
    }
 
89
    close(SHA);
 
90
    $? == 0 or die "OpenSSL failed to calculate SHA1 hashes\n";
 
91
}
 
92
#*******************************************************************************
 
93
our %ext = ();
 
94
our @ext = ();
 
95
for my $xpi (@xpi) {
 
96
    if (my $ext = $xpi->{ext}) {
 
97
        push(@ext, $ext) unless (exists($ext{$ext}));
 
98
        push(@{$ext{$ext}}, $xpi);
 
99
    }
 
100
}
 
101
#*******************************************************************************
 
102
sub ser(*$$$$);
 
103
sub ser(*$$$$) {
 
104
    my ($file, $tree, $offs, $incr, $mode) = @_;
 
105
    my ($name, $data, $attr) = @$tree;
 
106
    my $sort = $mode && $name =~ s/^rdf:/RDF:/;
 
107
    if (ref($data)) {
 
108
        $attr = $attr? ' '.($mode? '': 'rdf:').'about="'.$attr.'"': '';
 
109
        print $file "$offs<$name$attr>\n";
 
110
        for my $item ($sort? sort({$a->[0] cmp $b->[0]} @$data): @$data) {
 
111
            ser($file, $item, $offs.$incr, $incr, $mode);
 
112
        }
 
113
        print $file "$offs</$name>\n";
 
114
    } elsif ($name ne 'em:signature') {
 
115
        print $file "$offs<$name>$data</$name>\n";
 
116
    } elsif (!$mode) {
 
117
        print $file "$offs<$name>\n";
 
118
        print $file "$offs$incr", substr($data, 0, 64, ''), "\n" while ($data);
 
119
        print $file "$offs</$name>\n";
 
120
    }
 
121
}
 
122
#*******************************************************************************
 
123
use constant BUFFER => 32768; # Must be BUFFER > max(length($signature))
 
124
sub sig($) {
 
125
    open(RDF, '>', $tmp) or die "Failed to create a temporary file: $!\n";
 
126
    binmode(RDF);
 
127
    ser(*RDF, shift, '', '  ', 1);
 
128
    close(RDF);
 
129
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
130
    open(SIG, '-|', qq[openssl dgst -sha512 -sign "$pem" -binary "$tmp"])
 
131
        or die "Failed to start OpenSSL to generate the signature: $!\n";
 
132
    binmode(SIG);
 
133
    my $body;
 
134
    my $size = read(SIG, $body, BUFFER);
 
135
    close(SIG);
 
136
    $? == 0 or die "OpenSSL failed to generate the signature\n";
 
137
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
138
    if (($size > 0) && ($size < BUFFER) && ($size == length($body))) {
 
139
        my $asn1 = Convert::ASN1->new(encoding => 'DER');
 
140
        $asn1->prepare(q<
 
141
            Algorithm ::= SEQUENCE {
 
142
                oid OBJECT IDENTIFIER,
 
143
                opt ANY OPTIONAL
 
144
            }
 
145
            Signature ::= SEQUENCE {
 
146
                alg Algorithm,
 
147
                sig BIT STRING
 
148
            }
 
149
        >);
 
150
        my $data = $asn1->encode(sig => $body,
 
151
            alg => {oid => sha512WithRSAEncryption()});
 
152
        if (defined($data)) {
 
153
            return encode_base64($data, '');
 
154
        } else {
 
155
            die "Failed to encode the generated signature: ".$asn1->error."\n";
 
156
        }
 
157
    } else {
 
158
        die "Failed to obtain the generated signature from OpenSSL\n";
 
159
    }
 
160
}
 
161
#*******************************************************************************
 
162
if (defined($rdf)) {
 
163
    die "Signing an existing update manifest is not supported yet\n";
 
164
}
 
165
#*******************************************************************************
 
166
for my $ext (values(%ext)) {
 
167
    @$ext = sort {
 
168
        my @ab = map {$_->{ver}} ($a, $b);
 
169
        my ($ax, $bx) = map {[map {($_ eq '*')? '*': [('0') x !/^-?\d/,
 
170
            split /(?<=\d)(?=\D)|(?<=[^-\d])(?=-?\d)/]} split /\./]} @ab;
 
171
        push(@$ax, (['0']) x ($#$bx - $#$ax)) if ($#$ax < $#$bx);
 
172
        push(@$bx, (['0']) x ($#$ax - $#$bx)) if ($#$bx < $#$ax);
 
173
        my $cmp = 0;
 
174
        for my $ay (@$ax) {
 
175
            my $by = shift @$bx;
 
176
            if (ref($ay) && ref($by)) {
 
177
                foreach my $i (0..(($#$ay > $#$by)? $#$ay: $#$by)) {
 
178
                    my ($az, $bz) = ($ay->[$i], $by->[$i]);
 
179
                    $cmp = ($i % 2)? ((defined($bz) <=> defined($az)) ||
 
180
                        ($az cmp $bz)): (($az || 0) <=> ($bz || 0));
 
181
                    return $cmp if $cmp;
 
182
                }
 
183
            } else {
 
184
                $cmp = !ref($ay) <=> !ref($by);
 
185
            }
 
186
            return $cmp if $cmp;
 
187
        }
 
188
        return 0;
 
189
    } @$ext if (@$ext > 1);
 
190
}
 
191
#*******************************************************************************
 
192
our @rdf = map {
 
193
    ['rdf:Description' => [
 
194
        ['em:updates' => [
 
195
            ['rdf:Seq' => [
 
196
                (map {
 
197
                    my $upd = $_;
 
198
                    ['rdf:li' => [
 
199
                        ['rdf:Description' => [
 
200
                            ['em:version' => $upd->{ver}],
 
201
                            (map {
 
202
                                ['em:targetApplication' => [
 
203
                                    ['rdf:Description' => [
 
204
                                        ['em:id' => $_->{app}],
 
205
                                        ['em:minVersion' => "3.0"],
 
206
                                        ['em:maxVersion' => $_->{max}],
 
207
                                        ['em:updateLink' => $upd->{url}],
 
208
                                        exists($upd->{sha})?
 
209
                                        ['em:updateHash' => $upd->{sha}]: (),
 
210
                                        exists($upd->{inf})?
 
211
                                        ['em:updateInfoURL' => $upd->{inf}]: ()
 
212
                                    ]]
 
213
                                ]]
 
214
                            } @{$upd->{app}})
 
215
                        ]]
 
216
                    ]]
 
217
                } @{$ext{$_}})
 
218
            ]]
 
219
        ]]
 
220
    ], 'urn:mozilla:extension:'.$_]
 
221
} @ext;
 
222
#*******************************************************************************
 
223
if ($pem) {
 
224
    push(@{$_->[1]}, ['em:signature' => sig($_)]) for (@rdf);
 
225
}
 
226
#*******************************************************************************
 
227
use constant INDENT => ' ' x 4;
 
228
if (defined($out)) {
 
229
    open(RDF, '>', $out) or die "Failed to open the output file '$out': $!\n";
 
230
} else {
 
231
    open(RDF, '>&', \*STDOUT) or die "Failed to open output stream: $!\n";
 
232
}
 
233
print RDF <<'RDF';
 
234
<?xml version="1.0"?>
 
235
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 
236
        xmlns:em="http://www.mozilla.org/2004/em-rdf#">
 
237
RDF
 
238
ser(*RDF, $_, INDENT(), INDENT(), 0) for (@rdf);
 
239
print RDF "</rdf:RDF>\n";
 
240
close(RDF);
 
241
#*******************************************************************************
 
242
#END { unlink($tmp) if (defined($tmp) && (-e $tmp)); }
 
243
#*******************************************************************************
 
244
=pod
 
245
 
 
246
=head1 NAME
 
247
 
 
248
mexumgen - Mozilla Extension Update Manifest Generator, version 1.1
 
249
 
 
250
=head1 SYNOPSIS
 
251
 
 
252
menumgen [-h | -k key.pem] [-w] [-o out.rdf] etc...
 
253
 
 
254
=head1 ARGUMENTS
 
255
 
 
256
=over 2
 
257
 
 
258
=item B<-h>
 
259
 
 
260
Calculate SHA1 hash of every installation package and include it in the update
 
261
manifest. If B<-k> option is present this is implied.
 
262
 
 
263
=item B<-k key.pem>
 
264
 
 
265
The private key to sign the update manifest with. Typically it is 1024-bit RSA
 
266
key in PEM format. If this parameter is omitted the update manifest will not be
 
267
signed.
 
268
 
 
269
=item B<-w>
 
270
 
 
271
Indicates that C<updateInfoURL> (a.k.a "What's new") should be included in the
 
272
generated update manifest. This changes the meaning of the tail of the command
 
273
line (see B<etc> below).
 
274
 
 
275
=item B<-o out.rdf>
 
276
 
 
277
The output update manifest file (generated and signed). This can be the same
 
278
file as specified with B<-i> option. If this parameter is omitted the resulting
 
279
update manifest will be written to the standard output.
 
280
 
 
281
=item B<etc>
 
282
 
 
283
The remaining command line arguments specify the installation packages to be
 
284
used and the corresponding URLs.
 
285
 
 
286
If B<-w> option is present, the number of remaining
 
287
arguments must be multiple of 3. Each group of 3 arguments specifies (in that
 
288
order): the path to the installation package in the local file system, the URL
 
289
where that package is going to be available, the URL of the "What's new" page
 
290
for the package.
 
291
 
 
292
If B<-w> option is not present, the number
 
293
of remaining arguments must be multiple of 2. Each pair of arguments specifies
 
294
the path to the installation package in the local file system and the URL where
 
295
that package is going to be available.
 
296
 
 
297
In other words, the B<-w> option indicates whether "What's new" URLs must be
 
298
present in the list of the installation packages.
 
299
 
 
300
=back
 
301
 
 
302
=head1 DESCRIPTION
 
303
 
 
304
The F<install.rdf> file found in each installation package specified in the
 
305
command line is parsed to retrieve the information about the extension (I<id>,
 
306
I<version>) and about the target application(s) (I<id>, I<minVersion>,
 
307
I<maxVersion>). Also, if the update manifest is to be signed or if B<-h>
 
308
command line option is present, the SHA1 hash of each installation package
 
309
(xpi file) is calculated. That information is used to construct a new update
 
310
manifest.
 
311
 
 
312
=head1 EXAMPLE
 
313
 
 
314
menumgen -k key.pem -w -o update.rdf extension.xpi
 
315
    http://www.example.com/download/extension.xpi
 
316
    http://www.example.com/extension/update.xhtml
 
317
 
 
318
=head1 KNOWN ISSUES
 
319
 
 
320
The I<targetPlatform> from the installation packages is not taken into account.
 
321
 
 
322
If the key is encrypted the password must be entered interactively, there is
 
323
no way to specify it on the command line.
 
324
 
 
325
Signing an existing update manifest is not supported in the current version.
 
326
 
 
327
=head1 DEPENDENCIES
 
328
 
 
329
Convert::ASN1, XML::Parser, RDF::Core, openssl, unzip
 
330
 
 
331
=head1 HOME PAGE
 
332
 
 
333
L<http://www.softlights.net/projects/mexumgen/>
 
334
 
 
335
=head1 DOWNLOAD
 
336
 
 
337
L<http://www.softlights.net/projects/mexumgen/mexumgen.zip>
 
338
 
 
339
=head1 AUTHORS
 
340
 
 
341
Copyright (C) 2008 Sergei Zhirikov (sfzhi@yahoo.com)
 
342
 
 
343
=cut