File Coverage

blib/lib/Net/iTMS/Request.pm
Criterion Covered Total %
statement 28 30 93.3
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 38 40 95.0


line stmt bran cond sub pod time code
1             package Net::iTMS::Request;
2             #
3             # Written by Thomas R. Sibley,
4             #
5             # Information on properly fetching the URLs and decrypting
6             # the content thanks to Jason Rohrer.
7             #
8 1     1   6 use warnings;
  1         2  
  1         37  
9 1     1   5 use strict;
  1         2  
  1         38  
10              
11 1     1   11 use vars '$VERSION';
  1         2  
  1         53  
12             $VERSION = '0.14';
13              
14 1     1   2410 use LWP::UserAgent;
  1         172415  
  1         61  
15 1     1   12 use HTTP::Request;
  1         2  
  1         27  
16              
17 1     1   6 use URI::Escape qw//;
  1         2  
  1         20  
18              
19 1     1   1703 use Crypt::CBC;
  1         9784  
  1         46  
20 1     1   1077 use Crypt::Rijndael;
  1         928  
  1         29  
21 1     1   7 use Digest::MD5;
  1         2  
  1         50  
22              
23 1     1   552 use XML::Twig;
  0            
  0            
24              
25             use Net::iTMS::Error;
26              
27             =head1 NAME
28              
29             Net::iTMS::Request - Library for making requests to the iTMS
30              
31             =head1 DESCRIPTION
32              
33             Net::iTMS::Request handles the fetching, decrypting, and uncompressing of
34             content from the iTunes Music Store.
35              
36             =head1 METHODS
37              
38             All methods return C on error and (should) set an error message,
39             which is available through the C method. (Unless noted otherwise.)
40              
41             =over 12
42              
43             =item C<< new([ debug => 1, [...] ]) >>
44              
45             Takes an argument list of C value> pairs. The options available
46             are:
47              
48             =over 24
49              
50             =item C<< debug => 0 or 1 >>
51              
52             If set to a true value, debug messages to be printed to STDERR.
53              
54             =item C<< show_xml => 0 or 1 >>
55              
56             If set to a true value, the XML fetched during each request will printed
57             to STDERR. The C option must also be set to true for the XML to
58             print.
59              
60             =back
61              
62             Returns a blessed hashref (object) for Net::iTMS::Request.
63              
64             =cut
65             sub new {
66             my ($class, %opt) = @_;
67              
68             my $ua = LWP::UserAgent->new;
69             $ua->agent('iTunes/4.2 (Macintosh; U; PPC Mac OS X 10.2)');
70            
71             return bless {
72             error => '',
73             debug => defined $opt{debug} ? $opt{debug} : 0,
74             show_xml=> defined $opt{show_xml} ? $opt{show_xml} : 0,
75             _ua => $ua,
76             _parser => 'XML::Twig',
77             _url => {
78             search => 'http://phobos.apple.com/WebObjects/MZSearch.woa/wa/com.apple.jingle.search.DirectAction/search?term=',
79             viewAlbum => 'http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wa/viewAlbum?playlistId=',
80             advancedSearch => 'http://phobos.apple.com/WebObjects/MZSearch.woa/wa/advancedSearchResults?',
81             # Albums ordered by best-sellers
82             viewArtist => 'http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wa/viewArtist?sortMode=2&artistId=',
83             biography => 'http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wa/com.apple.jingle.app.store.DirectAction/biography?artistId=',
84             influencers => 'http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wa/com.apple.jingle.app.store.DirectAction/influencers?artistId=',
85             browseArtist => 'http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wa/com.apple.jingle.app.store.DirectAction/browseArtist?artistId=',
86             },
87             }, $class;
88             }
89              
90             =item C<< url($url, [$append ,[{ gunzip => 1, decrypt => 0 }]]) >>
91              
92             This is one of the lower-level methods used internally.
93              
94             It takes a URL (that should be for the iTMS) as the first argument.
95             If the first argument does NOT start with "http", then it will
96             be taken as a key to the internal hash of URLs (C<< $request->{_url} >>)
97             and the appropriate stored URL will be used.
98              
99             The optional second argument is appended to the URL; this is useful
100             pretty much only when the first argument isn't a real URL and you
101             want to append query values to the end of the stored URL.
102              
103             The optional third argument is a hashref of options. In most cases
104             it is not needed, however, the available options are:
105              
106             =over 24
107              
108             =item C<< gunzip => 0 or 1 >>
109              
110             A true value means the (presumably) gzipped content is gunzipped. A false
111             value means it is not.
112              
113             Default is 1 (unzip content).
114              
115             =item C<< decrypt => 0, 1, or 2 >>
116              
117             A true value other than 2 means the content retrieved from the URL is first
118             decrypted after fetching if it appears to be encrypted (that is, if no
119             initialization vector was passed as a response header for the request).
120             A false value means no decryption is done at all. A value of 2 means
121             decryption will be forced no matter what.
122              
123             Default is 1 ("intelligent" decrypt), which should work for most, if not all,
124             cases.
125              
126             =back
127              
128             =cut
129             sub url {
130             my ($self, $url, $args) = @_;
131            
132             my $opt = defined $_[3] ? $_[3] : { };
133            
134             $url = $self->{_url}->{$url}
135             unless $url =~ /^http/;
136            
137             if (defined $args) {
138             if (ref $args eq 'HASH') {
139             my $i = 0;
140             for my $key (keys %$args) {
141             $url .= ($i < 1 ? "" : "&")
142             . URI::Escape::uri_escape($key)
143             . "="
144             . URI::Escape::uri_escape($args->{$key});
145             $i++;
146             }
147             }
148             else {
149             $url .= URI::Escape::uri_escape($args);
150             }
151             }
152            
153             my $xml = $self->_fetch_data($url, $opt)
154             or return undef;
155            
156             $self->_debug($xml)
157             if $self->{show_xml};
158             $self->_debug("Parsing $url");
159            
160             return $self->{_parser}->new->parse($xml)
161             || $self->_set_error('Error parsing XML!');
162             }
163              
164             sub _fetch_data {
165             my ($self, $url, $userOpt) = @_;
166            
167             return $self->_set_error('No URL specified!')
168             if not $url;
169            
170             $self->_debug('URL: ' . $url);
171            
172             my $opt = { gunzip => 1, decrypt => 1 };
173             if (defined $userOpt) {
174             for (qw/gunzip decrypt/) {
175             $opt->{$_} = $userOpt->{$_} if exists $userOpt->{$_};
176             }
177             }
178            
179             $self->_debug('Sending HTTP request...');
180             # Create and send request
181             my $req = HTTP::Request->new(GET => $url);
182             $self->_set_request_headers($req);
183            
184             my $res = $self->{_ua}->request($req);
185              
186             if (not $res->is_success) {
187             return $self->_set_error('HTTP request failed!' . "\n\n" . $req->as_string);
188             }
189              
190             $self->_debug('Successful request!');
191            
192             if ($opt->{decrypt}) {
193             $self->_debug('Decrypting content...');
194            
195             # Since the key is static, we can just hard-code it here
196             my $iTunesKey = pack 'H*', '8a9dad399fb014c131be611820d78895';
197              
198             #
199             # Create the AES CBC decryption object using the iTunes key and the
200             # initialization vector (x-apple-crypto-iv)
201             #
202             my $cbc = Crypt::CBC->new({
203             key => $iTunesKey,
204             cipher => 'Rijndael',
205             iv => pack ('H*', $res->header('x-apple-crypto-iv')),
206             regenerate_key => 0,
207             padding => 'standard',
208             prepend_iv => 0,
209             });
210              
211             # Try to intelligently determine whether content is actually
212             # encrypted. If it isn't, skip the decryption unless the caller
213             # explicitly wants us to decrypt (the decrypt option = 2).
214            
215             my $decrypted;
216            
217             if ($opt->{decrypt} == 2 or $res->header('x-apple-crypto-iv')) {
218             $decrypted = $cbc->decrypt($res->content);
219             } else {
220             $self->_debug(' Content looks unencrypted... skipping decryption');
221             $decrypted = $res->content;
222             }
223              
224             if ($opt->{gunzip}) {
225             $self->_debug('Uncompressing content...');
226              
227             return $self->_gunzip_data($decrypted);
228             } else {
229             return $decrypted;
230             }
231             }
232             elsif ($opt->{gunzip}) {
233             $self->_debug('Uncompressing content...');
234            
235             return $self->_gunzip_data($res->content);
236             }
237             else {
238             return $res->content;
239             }
240             }
241              
242             sub _gunzip_data {
243             my ($self, $data) = @_;
244            
245             # Use Compress::Zlib to decompress it
246             use Compress::Zlib qw();
247            
248             my $xml = Compress::Zlib::memGunzip($data);
249              
250             if (not defined $xml) {
251             return $self->_set_error('Error while uncompressing gzipped data: "',
252             $Compress::Zlib::gzerrno, '"');
253             }
254              
255             return $xml;
256             }
257              
258             sub _set_request_headers {
259             my $req = $_[1];
260             $req->header('Accept-Language' => 'en-us, en;q=0.50');
261             $req->header('Cookie' => 'countryVerified=1');
262             $req->header('Accept-Encoding' => 'gzip, x-aes-cbc');
263             }
264              
265             =back
266              
267             =head1 LICENSE
268              
269             Copyright 2004, Thomas R. Sibley.
270              
271             You may use, modify, and distribute this package under the same terms as Perl itself.
272              
273             =head1 AUTHOR
274              
275             Thomas R. Sibley, L
276              
277             =head1 SEE ALSO
278              
279             L, L
280              
281             =cut
282              
283             42;