| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691 |
- #!/usr/bin/perl
- use warnings;
- use strict;
- # Globals
- my $DEBUG = 0;
- #===============================================================
- # Define a sub-class of LWP::UserAgent so we can catch redirects
- #===============================================================
- package ZACH_UA;
- use base qw(LWP::UserAgent);
- #----------------------------------------------------------------------
- ## @fn redirect_ok
- # @brief If a redirect to a different url is okay
- # @param request
- # @param response
- # @return 1 is successful, 0 if not
- sub redirect_ok($$$) {
- my ($self, $request, $response) = @_;
- if ($DEBUG) {
- print STDERR "ZACH_UA::redirect_ok()\n";
- }
- $self->{'ZACH_redirect'} = $request->uri();
- if ($self->{'ZACH_redirect'} =~ /^file:\/\//i) {
- return 0;
- }
- return 1;
- }
- #----------------------------------------------------------------------
- ## @fn ZACH_clear_redirect
- # @brief Clears the ZACH_redirect value
- # @return void
- sub ZACH_clear_redirect($) {
- my ($self) = @_;
- if ($DEBUG) {
- print STDERR "ZACH_UA::clear_redirect()\n";
- }
- $self->{'ZACH_redirect'} = '';
- }
- #----------------------------------------------------------------------
- ## @fn ZACH_last_redirect
- # @brief Gets the ZACH_redirect value
- # @return ZACH_redirect
- sub ZACH_last_redirect($) {
- my ($self) = @_;
- if ($DEBUG) {
- print STDERR "ZACH_UA::last_redirect()\n";
- }
- return $self->{'ZACH_redirect'};
- }
- #===============================================================
- # Main Fetch.pm pacakge
- #===============================================================
- package Fetch;
- use URI::Escape;
- use HTTP::Request;
- use HTTP::Cookies;
- use Scalar::Util qw(reftype);
- #----------------------------------------------------------------------
- ## @fn new
- # @brief Constructor
- # @param class
- # @return self
- sub new {
- # Grab the class name
- my ($class) = shift(@_);
- # Grab the named parameters
- my (%params) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::new()\n";
- }
- # Init self
- my $self = {};
- bless $self, $class;
- # Set the defaults
- $self->{'delay'} = 5;
- $self->{'timeout'} = 60;
- $self->{'redial'} = 10;
- $self->{'cookiefile'} = undef();
- $self->{'uas'} = 'zachbot/2.0 <zach@kotlarek.com>';
- $self->{'content_type'} = '';
- $self->{'post_content'} = '';
- $self->{'headers'} = {};
- $self->{'referer'} = '';
- $self->{'url'} = '';
- $self->{'file'} = undef();
- $self->{'status_code'} = 400;
- $self->{'content'} = '';
- $self->{'cookies'} = undef();
- $self->{'redirect_uri'} = '';
- $self->{'ua'} = undef();
- $self->{'print_comments'} = 1;
- # Saved any provided parameters
- $self->delay($params{'delay'});
- $self->timeout($params{'timeout'});
- $self->redial($params{'redial'});
- $self->cookiefile($params{'cookiefile'});
- $self->uas($params{'uas'});
- $self->headers($params{'headers'});
- # Print Comments is special
- if (defined($params{'print_comments'}) && !$params{'print_comments'}) {
- $self->{'print_comments'} = 0;
- }
- if (defined($params{'no_headers'}) && $params{'no_headers'}) {
- $self->{'print_comments'} = 0;
- }
- # Initialize an LWP object
- $self->Fetch::init_ua();
- # Redial less when debugging
- if ($DEBUG) {
- $self->redial(1);
- }
- # Return a reference to ourselves
- return $self;
- }
- #----------------------------------------------------------------------
- ## @fn init_ua
- # @brief Initialize an LWP object
- # @return void
- sub init_ua() {
- my ($self) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::init_ua()\n";
- }
- # Construct the agent
- $self->{'ua'} = ZACH_UA->new();
- if ($DEBUG) {
- eval {
- $self->{'ua'}->add_handler('request_send', sub { shift->dump; print "\n\n"; return });
- $self->{'ua'}->add_handler('response_done', sub { shift->dump; print "\n\n"; return });
- };
- }
- $self->{'ua'}->agent($self->{'uas'});
- $self->{'ua'}->timeout($self->{'timeout'});
- push(@{ $self->{'ua'}->requests_redirectable }, 'POST');
- # Add a cookie jar, if requested
- if (defined($self->{'cookiefile'})) {
- $self->{'cookies'} = HTTP::Cookies->new(
- 'file' => $self->{'cookiefile'},
- 'autosave' => 1,
- 'ignore_discard' => 1
- );
- $self->{'ua'}->cookie_jar($self->{'cookies'});
- }
- # Disable SSL hostname verification
- $self->{'ua'}->ssl_opts('verify_hostname' => 0);
- }
- #----------------------------------------------------------------------
- ## @fn delay
- # @brief Set or get the Delay
- # @param delay
- # @return delay
- sub delay {
- my ($self, $delay) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::delay()\n";
- }
- if (defined($delay)) {
- $delay = int($delay);
- if ($delay >= 0) {
- $self->{'delay'} = $delay;
- }
- }
- return $self->{'delay'};
- }
- #----------------------------------------------------------------------
- ## @fn Timeout
- # @brief Set or get the Timeout
- # @return timeout
- sub timeout {
- my ($self, $timeout) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::timeout()\n";
- }
- if (defined($timeout)) {
- $timeout = int($timeout);
- if ($timeout > 0) {
- $self->{'timeout'} = $timeout;
- }
- }
- return $self->{'timeout'};
- }
- #----------------------------------------------------------------------
- ## @fn redial
- # @brief Set or get the Redial
- # @param redial
- # @return redial
- sub redial {
- my ($self, $redial) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::redial()\n";
- }
- if (defined($redial)) {
- $redial = int($redial);
- if ($redial > 0) {
- $self->{'redial'} = $redial;
- }
- }
- return $self->{'redial'};
- }
- #----------------------------------------------------------------------
- ## @fn cookiefile
- # @brief Set or get the Cookie File
- # @param cookiefile
- # @return cookiefile
- sub cookiefile {
- my ($self, $cookiefile) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::cookiefile()\n";
- }
- if (defined($cookiefile) && (!defined($self->{'cookiefile'}) || $cookiefile ne $self->{'cookiefile'})) {
- $self->{'cookiefile'} = $cookiefile;
- $self->init_ua();
- }
- return $self->{'cookiefile'};
- }
- #----------------------------------------------------------------------
- ## @fn uas
- # @brief Set or get the User Agent String
- # @param uas
- # @return uas
- sub uas {
- my ($self, $uas) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::uas()\n";
- }
- if (defined($uas) && length($uas) && $uas ne $self->{'uas'}) {
- $self->{'uas'} = $uas;
- $self->Fetch::init_ua();
- }
- return $self->{'uas'};
- }
- #----------------------------------------------------------------------
- ## @fn headers
- # @brief Set or get the Headers
- # @param data
- # @return headers
- sub headers {
- my $self = shift(@_);
- my ($data) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::headers()\n";
- }
- if (defined($data)) {
- if (reftype($data) eq 'HASH') {
- %{ $self->{'headers'} } = %{$data};
- } else {
- %{ $self->{'headers'} } = %{@_};
- }
- }
- return %{ $self->{'headers'} };
- }
- #----------------------------------------------------------------------
- ## @fn url
- # @brief Set or get the URL
- # @param url
- # @return url
- sub url {
- my ($self, $url) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::url()\n";
- }
- if (defined($url)) {
- $self->{'referer'} = $self->{'url'};
- $self->{'url'} = $url;
- }
- return $self->{'url'};
- }
- #----------------------------------------------------------------------
- ## @fn file
- # @brief Set or get the file
- # @param file
- # @return file
- sub file {
- my ($self, $file) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::file()\n";
- }
- if (defined($file)) {
- $self->{'file'} = $file;
- }
- return $self->{'file'};
- }
- #----------------------------------------------------------------------
- ## @fn post_content
- # @brief Set or get the POST content
- # @param data
- # @return post_content
- sub post_content {
- my ($self) = shift(@_);
- my ($data) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::post_content()\n";
- }
- my $content = '';
- # Return immediately if there is no change
- if (!defined($data)) {
- return $self->{'post_content'};
- }
- # Set the content type
- $self->{'content_type'} = 'application/x-www-form-urlencoded';
- # Skip processing is the data is empty
- my $type = reftype($data);
- if (defined($type) || length($data) > 0) {
- # Convert the hash to a string
- if (defined($type) && $type eq 'HASH') {
- foreach my $key (keys(%{$data})) {
- if (defined($content) && length($content)) {
- $content .= '&';
- }
- # Ensure the value is always defined (even if empty)
- my $value = $data->{$key};
- if (!$value) {
- $value = '';
- }
- # URL encode if necessary
- if ($key =~ /[^\w\%]/) {
- $key = uri_escape($key);
- }
- if ($value =~ /[^\w\%]/) {
- $value = uri_escape($value);
- }
- # Append the string
- $content .= $key . '=' . $value;
- }
- } else {
- # No conversion needed for strings
- $content = $data;
- }
- }
- # Save and return
- $self->{'post_content'} = $content;
- if ($DEBUG) {
- print STDERR 'Fetch::post_content(' . length($content) . '): ' . $content . "\n";
- }
- return $self->{'post_content'};
- }
- #----------------------------------------------------------------------
- ## @fn post_content_multi
- # @brief Set or get the POST content multipart
- # @param data
- # @param boundary
- # @return post_content
- sub post_content_multi {
- my ($self, $data, $boundary) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::post_content_multi()\n";
- }
- my $content = '';
- # Return immediately if there is no change
- if (!defined($data)) {
- return $self->{'post_content'};
- }
- # Pick a boundary if we need one
- if (!defined($boundary) || length($boundary) < 5) {
- $boundary = '';
- while (length($boundary) < 10) {
- $boundary .= chr(rand() * 127);
- $boundary =~ s/[^\w]//g;
- }
- }
- # Set the content type
- $self->{'content_type'} = 'multipart/form-data; boundary=' . $boundary;
- # Skip processing is the data is empty (or invalid)
- my $type = reftype($data);
- if (defined($type) && $type eq 'HASH') {
- # Convert the hash to a string
- foreach my $key (keys(%{$data})) {
- my $name = $key;
- if (ref($data->{$key}) ne 'ARRAY') {
- my @tmp = ($data->{$key});
- $data->{$key} = \@tmp;
- }
- foreach my $val (@{ $data->{$key} }) {
- $content .= '--' . $boundary . "\r\n";
- $content .= 'Content-Disposition: form-data; name="' . $name . "\"\r\n\r\n";
- $content .= $val . "\r\n";
- }
- }
- # End the multipart form
- $content .= '--' . $boundary . '--';
- }
- # Save and return
- $self->{'post_content'} = $content;
- if ($DEBUG) {
- print STDERR 'Fetch::post_content_multi(' . length($content) . "):\n" . $content . "\n";
- }
- return $self->{'post_content'};
- }
- #----------------------------------------------------------------------
- ## @fn status_code
- # @brief Get the Status Code
- # @return status_code
- sub status_code() {
- my ($self) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::status_code()\n";
- }
- return $self->{'status_code'};
- }
- #----------------------------------------------------------------------
- ## @fn last_redirect
- # @brief Get the redirect URI
- # @return redirect_uri
- sub last_redirect() {
- my ($self) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::redirect_uri()\n";
- }
- return $self->{'redirect_uri'};
- }
- #----------------------------------------------------------------------
- ## @fn fetch
- # @brief Performs a Fetch
- # @return status code
- sub fetch() {
- my ($self) = shift(@_);
- my (%params) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::fetch()\n";
- }
- my ($request, $response, $host);
- # Save the URL and filename, if any
- $self->Fetch::url($params{'url'});
- $self->Fetch::file($params{'file'});
- # Decide if we care about the result code
- my $nocheck = 0;
- if ($params{'nocheck'}) {
- $nocheck = 1;
- }
- # Check the URL
- if (!length($self->Fetch::url())) {
- $self->{'status_code'} = 400;
- return $self->Fetch::status_code();
- }
- # Create a GET or POST request
- if (length($self->Fetch::post_content())) {
- $request = HTTP::Request->new('POST' => $self->Fetch::url());
- $request->header('Content-Type' => $self->{'content_type'});
- $request->header('Content-Length' => length($self->Fetch::post_content()));
- $request->content($self->Fetch::post_content());
- if ($DEBUG) {
- print STDERR 'Fetching via POST: ' . $self->Fetch::url() . "\n";
- }
- } else {
- $request = HTTP::Request->new('GET' => $self->Fetch::url());
- if ($DEBUG) {
- print STDERR 'Fetching via GET: ' . $self->Fetch::url() . "\n";
- }
- }
- # Always add the 'Host' header
- # But allow headers() to override the value
- if (!defined($self->{'headers'}{'Host'}) || length($self->{'headers'}{'Host'}) < 1) {
- my ($host) = $self->Fetch::url() =~ /\w+:\/\/([^\/]+)/;
- $request->header('Host' => $host);
- if ($DEBUG) {
- print STDERR 'Setting host header: ' . $host . "\n";
- }
- }
- # Always add the 'Referer' header
- # But allow headers() to override the value
- if (!defined($self->{'headers'}{'Referer'}) || length($self->{'headers'}{'Referer'}) < 1) {
- $request->header('Referer' => $self->{'referer'});
- if ($DEBUG) {
- print STDERR 'Setting referer header: ' . $self->{'referer'} . "\n";
- }
- }
- # Always add the 'Accept-Encoding' header
- # But allow headers() to override the value
- if (!defined($self->{'headers'}{'Accept-Encoding'}) || length($self->{'headers'}{'Accept-Encoding'}) < 1) {
- $request->header('Accept-Encoding' => HTTP::Message::decodable);
- if ($DEBUG) {
- print STDERR 'Setting Accept-Encoding header: ' . HTTP::Message::decodable . "\n";
- }
- }
- # Append any extra headers
- foreach my $key (keys(%{ $self->{'headers'} })) {
- $request->header($key => $self->{'headers'}{$key});
- if ($DEBUG) {
- print STDERR 'Setting header: ' . $key . ' => ' . $self->{'headers'}{$key} . "\n";
- }
- }
- # Dial until we get results
- my $connectcount = $self->Fetch::redial();
- GET: {
- # Limit redials
- if (!$connectcount) {
- last GET;
- }
- $connectcount--;
- # Run the request
- $self->{'ua'}->ZACH_clear_redirect();
- $response = $self->{'ua'}->request($request);
- $self->{'redirect_uri'} = $self->{'ua'}->ZACH_last_redirect();
- if (defined($self->{'cookies'})) {
- $self->{'cookies'}->extract_cookies($response);
- }
- # Check for errors, loop if we aren't happy
- if ($nocheck) {
- last GET;
- } elsif ($response->status_line =~ /^200/) {
- last GET;
- } else {
- sleep($self->Fetch::delay() * (rand(2) + 0.5));
- redo GET;
- }
- }
- # Grab the status code
- ($self->{'status_code'}) = $response->status_line =~ /^(\d+)\s/;
- # Grab the content for future use
- $self->{'content'} = $response->decoded_content();
- # Save the output (save() does its own validity checks)
- $self->Fetch::save('nocheck' => $nocheck);
- # Write any new cookies to disk
- if (defined($self->{'cookies'}) && ref($self->{'cookies'})) {
- $self->{'cookies'}->save();
- }
- # Return the status code
- return $self->Fetch::status_code();
- }
- #----------------------------------------------------------------------
- ## @fn content
- # @brief Get the Content
- # @return data
- sub content() {
- my ($self) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::content()\n";
- }
- # Extract content (if any)
- my $data = $self->{'content'};
- if (!defined($data)) {
- $data = '';
- }
- # Standardize line endings
- $data =~ s/\r\n/\n/;
- # Return an array when requested
- if (wantarray) {
- return split(/\n/, $data);
- }
- # Otherwise return a string
- return $data;
- }
- #----------------------------------------------------------------------
- ## @fn save
- # @brief Save
- # @param params
- # @return 1 if successful, 0 if not
- sub save() {
- my ($self) = shift(@_);
- if ($DEBUG) {
- print STDERR "Fetch::save()\n";
- }
- my (%params) = @_;
- # Optionally update the file name
- $self->Fetch::file($params{'file'});
- # Bail on bad HTTP result codes (if we're checking)
- if (!$params{'nocheck'}) {
- if ($self->Fetch::status_code() != 200) {
- print STDERR "Fetch::save: Refusing to save file with bad status code\n";
- return 1;
- }
- }
- # Bail on bad file names -- but don't die, because we don't always want to save
- if (!defined($self->Fetch::file()) || !length($self->Fetch::file())) {
- return 1;
- }
- # Bail on I/O errors
- if (!open(OUT, '>', $self->Fetch::file())) {
- die('Fetch::save: Unable to open output file: ' . $self->Fetch::file() . ': ' . $! . "\n");
- }
- binmode OUT, ':encoding(UTF-8)';
- # Print out the URL, the post content (if any)
- if (defined($self->{'print_comments'}) && $self->{'print_comments'}) {
- print OUT '<!-- ' . $self->Fetch::url() . ' -->' . "\n";
- if (length($self->Fetch::post_content())) {
- print OUT '<!-- ' . $self->Fetch::post_content() . ' -->' . "\n";
- }
- print OUT "\n\n";
- }
- # Print the document body
- print OUT $self->Fetch::content() . "\n";
- close(OUT);
- # Return 0 on success
- return 0;
- }
- #----------------------------------------------------------------------
- ## @fn DESTROY
- # @brief Deconstructor
- sub DESTROY() {
- my ($self) = @_;
- if ($DEBUG) {
- print STDERR "Fetch::DESTROY()\n";
- }
- $self->{'cookies'} = undef();
- $self->{'ua'} = undef();
- }
- # Packages must return true
- return 1;
|