package Net::iTMS::Request;
#
# Written by Thomas R. Sibley,
#
# Information on properly fetching the URLs and decrypting
# the content thanks to Jason Rohrer.
#
use warnings;
use strict;
use vars '$VERSION';
$VERSION = '0.14';
use LWP::UserAgent;
use HTTP::Request;
use URI::Escape qw//;
use Crypt::CBC;
use Crypt::Rijndael;
use Digest::MD5;
use XML::Twig;
use Net::iTMS::Error;
=head1 NAME
Net::iTMS::Request - Library for making requests to the iTMS
=head1 DESCRIPTION
Net::iTMS::Request handles the fetching, decrypting, and uncompressing of
content from the iTunes Music Store.
=head1 METHODS
All methods return C on error and (should) set an error message,
which is available through the C method. (Unless noted otherwise.)
=over 12
=item C<< new([ debug => 1, [...] ]) >>
Takes an argument list of C value> pairs. The options available
are:
=over 24
=item C<< debug => 0 or 1 >>
If set to a true value, debug messages to be printed to STDERR.
=item C<< show_xml => 0 or 1 >>
If set to a true value, the XML fetched during each request will printed
to STDERR. The C option must also be set to true for the XML to
print.
=back
Returns a blessed hashref (object) for Net::iTMS::Request.
=cut
sub new {
my ($class, %opt) = @_;
my $ua = LWP::UserAgent->new;
$ua->agent('iTunes/4.2 (Macintosh; U; PPC Mac OS X 10.2)');
return bless {
error => '',
debug => defined $opt{debug} ? $opt{debug} : 0,
show_xml=> defined $opt{show_xml} ? $opt{show_xml} : 0,
_ua => $ua,
_parser => 'XML::Twig',
_url => {
search => 'http://phobos.apple.com/WebObjects/MZSearch.woa/wa/com.apple.jingle.search.DirectAction/search?term=',
viewAlbum => 'http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wa/viewAlbum?playlistId=',
advancedSearch => 'http://phobos.apple.com/WebObjects/MZSearch.woa/wa/advancedSearchResults?',
# Albums ordered by best-sellers
viewArtist => 'http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wa/viewArtist?sortMode=2&artistId=',
biography => 'http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wa/com.apple.jingle.app.store.DirectAction/biography?artistId=',
influencers => 'http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wa/com.apple.jingle.app.store.DirectAction/influencers?artistId=',
browseArtist => 'http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wa/com.apple.jingle.app.store.DirectAction/browseArtist?artistId=',
},
}, $class;
}
=item C<< url($url, [$append ,[{ gunzip => 1, decrypt => 0 }]]) >>
This is one of the lower-level methods used internally.
It takes a URL (that should be for the iTMS) as the first argument.
If the first argument does NOT start with "http", then it will
be taken as a key to the internal hash of URLs (C<< $request->{_url} >>)
and the appropriate stored URL will be used.
The optional second argument is appended to the URL; this is useful
pretty much only when the first argument isn't a real URL and you
want to append query values to the end of the stored URL.
The optional third argument is a hashref of options. In most cases
it is not needed, however, the available options are:
=over 24
=item C<< gunzip => 0 or 1 >>
A true value means the (presumably) gzipped content is gunzipped. A false
value means it is not.
Default is 1 (unzip content).
=item C<< decrypt => 0, 1, or 2 >>
A true value other than 2 means the content retrieved from the URL is first
decrypted after fetching if it appears to be encrypted (that is, if no
initialization vector was passed as a response header for the request).
A false value means no decryption is done at all. A value of 2 means
decryption will be forced no matter what.
Default is 1 ("intelligent" decrypt), which should work for most, if not all,
cases.
=back
=cut
sub url {
my ($self, $url, $args) = @_;
my $opt = defined $_[3] ? $_[3] : { };
$url = $self->{_url}->{$url}
unless $url =~ /^http/;
if (defined $args) {
if (ref $args eq 'HASH') {
my $i = 0;
for my $key (keys %$args) {
$url .= ($i < 1 ? "" : "&")
. URI::Escape::uri_escape($key)
. "="
. URI::Escape::uri_escape($args->{$key});
$i++;
}
}
else {
$url .= URI::Escape::uri_escape($args);
}
}
my $xml = $self->_fetch_data($url, $opt)
or return undef;
$self->_debug($xml)
if $self->{show_xml};
$self->_debug("Parsing $url");
return $self->{_parser}->new->parse($xml)
|| $self->_set_error('Error parsing XML!');
}
sub _fetch_data {
my ($self, $url, $userOpt) = @_;
return $self->_set_error('No URL specified!')
if not $url;
$self->_debug('URL: ' . $url);
my $opt = { gunzip => 1, decrypt => 1 };
if (defined $userOpt) {
for (qw/gunzip decrypt/) {
$opt->{$_} = $userOpt->{$_} if exists $userOpt->{$_};
}
}
$self->_debug('Sending HTTP request...');
# Create and send request
my $req = HTTP::Request->new(GET => $url);
$self->_set_request_headers($req);
my $res = $self->{_ua}->request($req);
if (not $res->is_success) {
return $self->_set_error('HTTP request failed!' . "\n\n" . $req->as_string);
}
$self->_debug('Successful request!');
if ($opt->{decrypt}) {
$self->_debug('Decrypting content...');
# Since the key is static, we can just hard-code it here
my $iTunesKey = pack 'H*', '8a9dad399fb014c131be611820d78895';
#
# Create the AES CBC decryption object using the iTunes key and the
# initialization vector (x-apple-crypto-iv)
#
my $cbc = Crypt::CBC->new({
key => $iTunesKey,
cipher => 'Rijndael',
iv => pack ('H*', $res->header('x-apple-crypto-iv')),
regenerate_key => 0,
padding => 'standard',
prepend_iv => 0,
});
# Try to intelligently determine whether content is actually
# encrypted. If it isn't, skip the decryption unless the caller
# explicitly wants us to decrypt (the decrypt option = 2).
my $decrypted;
if ($opt->{decrypt} == 2 or $res->header('x-apple-crypto-iv')) {
$decrypted = $cbc->decrypt($res->content);
} else {
$self->_debug(' Content looks unencrypted... skipping decryption');
$decrypted = $res->content;
}
if ($opt->{gunzip}) {
$self->_debug('Uncompressing content...');
return $self->_gunzip_data($decrypted);
} else {
return $decrypted;
}
}
elsif ($opt->{gunzip}) {
$self->_debug('Uncompressing content...');
return $self->_gunzip_data($res->content);
}
else {
return $res->content;
}
}
sub _gunzip_data {
my ($self, $data) = @_;
# Use Compress::Zlib to decompress it
use Compress::Zlib qw();
my $xml = Compress::Zlib::memGunzip($data);
if (not defined $xml) {
return $self->_set_error('Error while uncompressing gzipped data: "',
$Compress::Zlib::gzerrno, '"');
}
return $xml;
}
sub _set_request_headers {
my $req = $_[1];
$req->header('Accept-Language' => 'en-us, en;q=0.50');
$req->header('Cookie' => 'countryVerified=1');
$req->header('Accept-Encoding' => 'gzip, x-aes-cbc');
}
=back
=head1 LICENSE
Copyright 2004, Thomas R. Sibley.
You may use, modify, and distribute this package under the same terms as Perl itself.
=head1 AUTHOR
Thomas R. Sibley, L
=head1 SEE ALSO
L, L
=cut
42;