Fetch.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. #!/usr/bin/perl
  2. use warnings;
  3. use strict;
  4. # Globals
  5. my $DEBUG = 0;
  6. #===============================================================
  7. # Define a sub-class of LWP::UserAgent so we can catch redirects
  8. #===============================================================
  9. package ZACH_UA;
  10. use base qw(LWP::UserAgent);
  11. #----------------------------------------------------------------------
  12. ## @fn redirect_ok
  13. # @brief If a redirect to a different url is okay
  14. # @param request
  15. # @param response
  16. # @return 1 is successful, 0 if not
  17. sub redirect_ok($$$) {
  18. my ($self, $request, $response) = @_;
  19. if ($DEBUG) {
  20. print STDERR "ZACH_UA::redirect_ok()\n";
  21. }
  22. $self->{'ZACH_redirect'} = $request->uri();
  23. if ($self->{'ZACH_redirect'} =~ /^file:\/\//i) {
  24. return 0;
  25. }
  26. return 1;
  27. }
  28. #----------------------------------------------------------------------
  29. ## @fn ZACH_clear_redirect
  30. # @brief Clears the ZACH_redirect value
  31. # @return void
  32. sub ZACH_clear_redirect($) {
  33. my ($self) = @_;
  34. if ($DEBUG) {
  35. print STDERR "ZACH_UA::clear_redirect()\n";
  36. }
  37. $self->{'ZACH_redirect'} = '';
  38. }
  39. #----------------------------------------------------------------------
  40. ## @fn ZACH_last_redirect
  41. # @brief Gets the ZACH_redirect value
  42. # @return ZACH_redirect
  43. sub ZACH_last_redirect($) {
  44. my ($self) = @_;
  45. if ($DEBUG) {
  46. print STDERR "ZACH_UA::last_redirect()\n";
  47. }
  48. return $self->{'ZACH_redirect'};
  49. }
  50. #===============================================================
  51. # Main Fetch.pm pacakge
  52. #===============================================================
  53. package Fetch;
  54. use URI::Escape;
  55. use HTTP::Request;
  56. use HTTP::Cookies;
  57. use Scalar::Util qw(reftype);
  58. #----------------------------------------------------------------------
  59. ## @fn new
  60. # @brief Constructor
  61. # @param class
  62. # @return self
  63. sub new {
  64. # Grab the class name
  65. my ($class) = shift(@_);
  66. # Grab the named parameters
  67. my (%params) = @_;
  68. if ($DEBUG) {
  69. print STDERR "Fetch::new()\n";
  70. }
  71. # Init self
  72. my $self = {};
  73. bless $self, $class;
  74. # Set the defaults
  75. $self->{'delay'} = 5;
  76. $self->{'timeout'} = 60;
  77. $self->{'redial'} = 10;
  78. $self->{'cookiefile'} = undef();
  79. $self->{'uas'} = 'zachbot/2.0 <zach@kotlarek.com>';
  80. $self->{'content_type'} = '';
  81. $self->{'post_content'} = '';
  82. $self->{'headers'} = {};
  83. $self->{'referer'} = '';
  84. $self->{'url'} = '';
  85. $self->{'file'} = undef();
  86. $self->{'status_code'} = 400;
  87. $self->{'content'} = '';
  88. $self->{'cookies'} = undef();
  89. $self->{'redirect_uri'} = '';
  90. $self->{'ua'} = undef();
  91. $self->{'print_comments'} = 1;
  92. # Saved any provided parameters
  93. $self->delay($params{'delay'});
  94. $self->timeout($params{'timeout'});
  95. $self->redial($params{'redial'});
  96. $self->cookiefile($params{'cookiefile'});
  97. $self->uas($params{'uas'});
  98. $self->headers($params{'headers'});
  99. # Print Comments is special
  100. if (defined($params{'print_comments'}) && !$params{'print_comments'}) {
  101. $self->{'print_comments'} = 0;
  102. }
  103. if (defined($params{'no_headers'}) && $params{'no_headers'}) {
  104. $self->{'print_comments'} = 0;
  105. }
  106. # Initialize an LWP object
  107. $self->Fetch::init_ua();
  108. # Redial less when debugging
  109. if ($DEBUG) {
  110. $self->redial(1);
  111. }
  112. # Return a reference to ourselves
  113. return $self;
  114. }
  115. #----------------------------------------------------------------------
  116. ## @fn init_ua
  117. # @brief Initialize an LWP object
  118. # @return void
  119. sub init_ua() {
  120. my ($self) = @_;
  121. if ($DEBUG) {
  122. print STDERR "Fetch::init_ua()\n";
  123. }
  124. # Construct the agent
  125. $self->{'ua'} = ZACH_UA->new();
  126. if ($DEBUG) {
  127. eval {
  128. $self->{'ua'}->add_handler('request_send', sub { shift->dump; print "\n\n"; return });
  129. $self->{'ua'}->add_handler('response_done', sub { shift->dump; print "\n\n"; return });
  130. };
  131. }
  132. $self->{'ua'}->agent($self->{'uas'});
  133. $self->{'ua'}->timeout($self->{'timeout'});
  134. push(@{ $self->{'ua'}->requests_redirectable }, 'POST');
  135. # Add a cookie jar, if requested
  136. if (defined($self->{'cookiefile'})) {
  137. $self->{'cookies'} = HTTP::Cookies->new(
  138. 'file' => $self->{'cookiefile'},
  139. 'autosave' => 1,
  140. 'ignore_discard' => 1
  141. );
  142. $self->{'ua'}->cookie_jar($self->{'cookies'});
  143. }
  144. # Disable SSL hostname verification
  145. $self->{'ua'}->ssl_opts('verify_hostname' => 0);
  146. }
  147. #----------------------------------------------------------------------
  148. ## @fn delay
  149. # @brief Set or get the Delay
  150. # @param delay
  151. # @return delay
  152. sub delay {
  153. my ($self, $delay) = @_;
  154. if ($DEBUG) {
  155. print STDERR "Fetch::delay()\n";
  156. }
  157. if (defined($delay)) {
  158. $delay = int($delay);
  159. if ($delay >= 0) {
  160. $self->{'delay'} = $delay;
  161. }
  162. }
  163. return $self->{'delay'};
  164. }
  165. #----------------------------------------------------------------------
  166. ## @fn Timeout
  167. # @brief Set or get the Timeout
  168. # @return timeout
  169. sub timeout {
  170. my ($self, $timeout) = @_;
  171. if ($DEBUG) {
  172. print STDERR "Fetch::timeout()\n";
  173. }
  174. if (defined($timeout)) {
  175. $timeout = int($timeout);
  176. if ($timeout > 0) {
  177. $self->{'timeout'} = $timeout;
  178. }
  179. }
  180. return $self->{'timeout'};
  181. }
  182. #----------------------------------------------------------------------
  183. ## @fn redial
  184. # @brief Set or get the Redial
  185. # @param redial
  186. # @return redial
  187. sub redial {
  188. my ($self, $redial) = @_;
  189. if ($DEBUG) {
  190. print STDERR "Fetch::redial()\n";
  191. }
  192. if (defined($redial)) {
  193. $redial = int($redial);
  194. if ($redial > 0) {
  195. $self->{'redial'} = $redial;
  196. }
  197. }
  198. return $self->{'redial'};
  199. }
  200. #----------------------------------------------------------------------
  201. ## @fn cookiefile
  202. # @brief Set or get the Cookie File
  203. # @param cookiefile
  204. # @return cookiefile
  205. sub cookiefile {
  206. my ($self, $cookiefile) = @_;
  207. if ($DEBUG) {
  208. print STDERR "Fetch::cookiefile()\n";
  209. }
  210. if (defined($cookiefile) && (!defined($self->{'cookiefile'}) || $cookiefile ne $self->{'cookiefile'})) {
  211. $self->{'cookiefile'} = $cookiefile;
  212. $self->init_ua();
  213. }
  214. return $self->{'cookiefile'};
  215. }
  216. #----------------------------------------------------------------------
  217. ## @fn uas
  218. # @brief Set or get the User Agent String
  219. # @param uas
  220. # @return uas
  221. sub uas {
  222. my ($self, $uas) = @_;
  223. if ($DEBUG) {
  224. print STDERR "Fetch::uas()\n";
  225. }
  226. if (defined($uas) && length($uas) && $uas ne $self->{'uas'}) {
  227. $self->{'uas'} = $uas;
  228. $self->Fetch::init_ua();
  229. }
  230. return $self->{'uas'};
  231. }
  232. #----------------------------------------------------------------------
  233. ## @fn headers
  234. # @brief Set or get the Headers
  235. # @param data
  236. # @return headers
  237. sub headers {
  238. my $self = shift(@_);
  239. my ($data) = @_;
  240. if ($DEBUG) {
  241. print STDERR "Fetch::headers()\n";
  242. }
  243. if (defined($data)) {
  244. if (reftype($data) eq 'HASH') {
  245. %{ $self->{'headers'} } = %{$data};
  246. } else {
  247. %{ $self->{'headers'} } = %{@_};
  248. }
  249. }
  250. return %{ $self->{'headers'} };
  251. }
  252. #----------------------------------------------------------------------
  253. ## @fn url
  254. # @brief Set or get the URL
  255. # @param url
  256. # @return url
  257. sub url {
  258. my ($self, $url) = @_;
  259. if ($DEBUG) {
  260. print STDERR "Fetch::url()\n";
  261. }
  262. if (defined($url)) {
  263. $self->{'referer'} = $self->{'url'};
  264. $self->{'url'} = $url;
  265. }
  266. return $self->{'url'};
  267. }
  268. #----------------------------------------------------------------------
  269. ## @fn file
  270. # @brief Set or get the file
  271. # @param file
  272. # @return file
  273. sub file {
  274. my ($self, $file) = @_;
  275. if ($DEBUG) {
  276. print STDERR "Fetch::file()\n";
  277. }
  278. if (defined($file)) {
  279. $self->{'file'} = $file;
  280. }
  281. return $self->{'file'};
  282. }
  283. #----------------------------------------------------------------------
  284. ## @fn post_content
  285. # @brief Set or get the POST content
  286. # @param data
  287. # @return post_content
  288. sub post_content {
  289. my ($self) = shift(@_);
  290. my ($data) = @_;
  291. if ($DEBUG) {
  292. print STDERR "Fetch::post_content()\n";
  293. }
  294. my $content = '';
  295. # Return immediately if there is no change
  296. if (!defined($data)) {
  297. return $self->{'post_content'};
  298. }
  299. # Set the content type
  300. $self->{'content_type'} = 'application/x-www-form-urlencoded';
  301. # Skip processing is the data is empty
  302. my $type = reftype($data);
  303. if (defined($type) || length($data) > 0) {
  304. # Convert the hash to a string
  305. if (defined($type) && $type eq 'HASH') {
  306. foreach my $key (keys(%{$data})) {
  307. if (defined($content) && length($content)) {
  308. $content .= '&';
  309. }
  310. # Ensure the value is always defined (even if empty)
  311. my $value = $data->{$key};
  312. if (!$value) {
  313. $value = '';
  314. }
  315. # URL encode if necessary
  316. if ($key =~ /[^\w\%]/) {
  317. $key = uri_escape($key);
  318. }
  319. if ($value =~ /[^\w\%]/) {
  320. $value = uri_escape($value);
  321. }
  322. # Append the string
  323. $content .= $key . '=' . $value;
  324. }
  325. } else {
  326. # No conversion needed for strings
  327. $content = $data;
  328. }
  329. }
  330. # Save and return
  331. $self->{'post_content'} = $content;
  332. if ($DEBUG) {
  333. print STDERR 'Fetch::post_content(' . length($content) . '): ' . $content . "\n";
  334. }
  335. return $self->{'post_content'};
  336. }
  337. #----------------------------------------------------------------------
  338. ## @fn post_content_multi
  339. # @brief Set or get the POST content multipart
  340. # @param data
  341. # @param boundary
  342. # @return post_content
  343. sub post_content_multi {
  344. my ($self, $data, $boundary) = @_;
  345. if ($DEBUG) {
  346. print STDERR "Fetch::post_content_multi()\n";
  347. }
  348. my $content = '';
  349. # Return immediately if there is no change
  350. if (!defined($data)) {
  351. return $self->{'post_content'};
  352. }
  353. # Pick a boundary if we need one
  354. if (!defined($boundary) || length($boundary) < 5) {
  355. $boundary = '';
  356. while (length($boundary) < 10) {
  357. $boundary .= chr(rand() * 127);
  358. $boundary =~ s/[^\w]//g;
  359. }
  360. }
  361. # Set the content type
  362. $self->{'content_type'} = 'multipart/form-data; boundary=' . $boundary;
  363. # Skip processing is the data is empty (or invalid)
  364. my $type = reftype($data);
  365. if (defined($type) && $type eq 'HASH') {
  366. # Convert the hash to a string
  367. foreach my $key (keys(%{$data})) {
  368. my $name = $key;
  369. if (ref($data->{$key}) ne 'ARRAY') {
  370. my @tmp = ($data->{$key});
  371. $data->{$key} = \@tmp;
  372. }
  373. foreach my $val (@{ $data->{$key} }) {
  374. $content .= '--' . $boundary . "\r\n";
  375. $content .= 'Content-Disposition: form-data; name="' . $name . "\"\r\n\r\n";
  376. $content .= $val . "\r\n";
  377. }
  378. }
  379. # End the multipart form
  380. $content .= '--' . $boundary . '--';
  381. }
  382. # Save and return
  383. $self->{'post_content'} = $content;
  384. if ($DEBUG) {
  385. print STDERR 'Fetch::post_content_multi(' . length($content) . "):\n" . $content . "\n";
  386. }
  387. return $self->{'post_content'};
  388. }
  389. #----------------------------------------------------------------------
  390. ## @fn status_code
  391. # @brief Get the Status Code
  392. # @return status_code
  393. sub status_code() {
  394. my ($self) = @_;
  395. if ($DEBUG) {
  396. print STDERR "Fetch::status_code()\n";
  397. }
  398. return $self->{'status_code'};
  399. }
  400. #----------------------------------------------------------------------
  401. ## @fn last_redirect
  402. # @brief Get the redirect URI
  403. # @return redirect_uri
  404. sub last_redirect() {
  405. my ($self) = @_;
  406. if ($DEBUG) {
  407. print STDERR "Fetch::redirect_uri()\n";
  408. }
  409. return $self->{'redirect_uri'};
  410. }
  411. #----------------------------------------------------------------------
  412. ## @fn fetch
  413. # @brief Performs a Fetch
  414. # @return status code
  415. sub fetch() {
  416. my ($self) = shift(@_);
  417. my (%params) = @_;
  418. if ($DEBUG) {
  419. print STDERR "Fetch::fetch()\n";
  420. }
  421. my ($request, $response, $host);
  422. # Save the URL and filename, if any
  423. $self->Fetch::url($params{'url'});
  424. $self->Fetch::file($params{'file'});
  425. # Decide if we care about the result code
  426. my $nocheck = 0;
  427. if ($params{'nocheck'}) {
  428. $nocheck = 1;
  429. }
  430. # Check the URL
  431. if (!length($self->Fetch::url())) {
  432. $self->{'status_code'} = 400;
  433. return $self->Fetch::status_code();
  434. }
  435. # Create a GET or POST request
  436. if (length($self->Fetch::post_content())) {
  437. $request = HTTP::Request->new('POST' => $self->Fetch::url());
  438. $request->header('Content-Type' => $self->{'content_type'});
  439. $request->header('Content-Length' => length($self->Fetch::post_content()));
  440. $request->content($self->Fetch::post_content());
  441. if ($DEBUG) {
  442. print STDERR 'Fetching via POST: ' . $self->Fetch::url() . "\n";
  443. }
  444. } else {
  445. $request = HTTP::Request->new('GET' => $self->Fetch::url());
  446. if ($DEBUG) {
  447. print STDERR 'Fetching via GET: ' . $self->Fetch::url() . "\n";
  448. }
  449. }
  450. # Always add the 'Host' header
  451. # But allow headers() to override the value
  452. if (!defined($self->{'headers'}{'Host'}) || length($self->{'headers'}{'Host'}) < 1) {
  453. my ($host) = $self->Fetch::url() =~ /\w+:\/\/([^\/]+)/;
  454. $request->header('Host' => $host);
  455. if ($DEBUG) {
  456. print STDERR 'Setting host header: ' . $host . "\n";
  457. }
  458. }
  459. # Always add the 'Referer' header
  460. # But allow headers() to override the value
  461. if (!defined($self->{'headers'}{'Referer'}) || length($self->{'headers'}{'Referer'}) < 1) {
  462. $request->header('Referer' => $self->{'referer'});
  463. if ($DEBUG) {
  464. print STDERR 'Setting referer header: ' . $self->{'referer'} . "\n";
  465. }
  466. }
  467. # Always add the 'Accept-Encoding' header
  468. # But allow headers() to override the value
  469. if (!defined($self->{'headers'}{'Accept-Encoding'}) || length($self->{'headers'}{'Accept-Encoding'}) < 1) {
  470. $request->header('Accept-Encoding' => HTTP::Message::decodable);
  471. if ($DEBUG) {
  472. print STDERR 'Setting Accept-Encoding header: ' . HTTP::Message::decodable . "\n";
  473. }
  474. }
  475. # Append any extra headers
  476. foreach my $key (keys(%{ $self->{'headers'} })) {
  477. $request->header($key => $self->{'headers'}{$key});
  478. if ($DEBUG) {
  479. print STDERR 'Setting header: ' . $key . ' => ' . $self->{'headers'}{$key} . "\n";
  480. }
  481. }
  482. # Dial until we get results
  483. my $connectcount = $self->Fetch::redial();
  484. GET: {
  485. # Limit redials
  486. if (!$connectcount) {
  487. last GET;
  488. }
  489. $connectcount--;
  490. # Run the request
  491. $self->{'ua'}->ZACH_clear_redirect();
  492. $response = $self->{'ua'}->request($request);
  493. $self->{'redirect_uri'} = $self->{'ua'}->ZACH_last_redirect();
  494. if (defined($self->{'cookies'})) {
  495. $self->{'cookies'}->extract_cookies($response);
  496. }
  497. # Check for errors, loop if we aren't happy
  498. if ($nocheck) {
  499. last GET;
  500. } elsif ($response->status_line =~ /^200/) {
  501. last GET;
  502. } else {
  503. sleep($self->Fetch::delay() * (rand(2) + 0.5));
  504. redo GET;
  505. }
  506. }
  507. # Grab the status code
  508. ($self->{'status_code'}) = $response->status_line =~ /^(\d+)\s/;
  509. # Grab the content for future use
  510. $self->{'content'} = $response->decoded_content();
  511. # Save the output (save() does its own validity checks)
  512. $self->Fetch::save('nocheck' => $nocheck);
  513. # Write any new cookies to disk
  514. if (defined($self->{'cookies'}) && ref($self->{'cookies'})) {
  515. $self->{'cookies'}->save();
  516. }
  517. # Return the status code
  518. return $self->Fetch::status_code();
  519. }
  520. #----------------------------------------------------------------------
  521. ## @fn content
  522. # @brief Get the Content
  523. # @return data
  524. sub content() {
  525. my ($self) = @_;
  526. if ($DEBUG) {
  527. print STDERR "Fetch::content()\n";
  528. }
  529. # Extract content (if any)
  530. my $data = $self->{'content'};
  531. if (!defined($data)) {
  532. $data = '';
  533. }
  534. # Standardize line endings
  535. $data =~ s/\r\n/\n/;
  536. # Return an array when requested
  537. if (wantarray) {
  538. return split(/\n/, $data);
  539. }
  540. # Otherwise return a string
  541. return $data;
  542. }
  543. #----------------------------------------------------------------------
  544. ## @fn save
  545. # @brief Save
  546. # @param params
  547. # @return 1 if successful, 0 if not
  548. sub save() {
  549. my ($self) = shift(@_);
  550. if ($DEBUG) {
  551. print STDERR "Fetch::save()\n";
  552. }
  553. my (%params) = @_;
  554. # Optionally update the file name
  555. $self->Fetch::file($params{'file'});
  556. # Bail on bad HTTP result codes (if we're checking)
  557. if (!$params{'nocheck'}) {
  558. if ($self->Fetch::status_code() != 200) {
  559. print STDERR "Fetch::save: Refusing to save file with bad status code\n";
  560. return 1;
  561. }
  562. }
  563. # Bail on bad file names -- but don't die, because we don't always want to save
  564. if (!defined($self->Fetch::file()) || !length($self->Fetch::file())) {
  565. return 1;
  566. }
  567. # Bail on I/O errors
  568. if (!open(OUT, '>', $self->Fetch::file())) {
  569. die('Fetch::save: Unable to open output file: ' . $self->Fetch::file() . ': ' . $! . "\n");
  570. }
  571. binmode OUT, ':encoding(UTF-8)';
  572. # Print out the URL, the post content (if any)
  573. if (defined($self->{'print_comments'}) && $self->{'print_comments'}) {
  574. print OUT '<!-- ' . $self->Fetch::url() . ' -->' . "\n";
  575. if (length($self->Fetch::post_content())) {
  576. print OUT '<!-- ' . $self->Fetch::post_content() . ' -->' . "\n";
  577. }
  578. print OUT "\n\n";
  579. }
  580. # Print the document body
  581. print OUT $self->Fetch::content() . "\n";
  582. close(OUT);
  583. # Return 0 on success
  584. return 0;
  585. }
  586. #----------------------------------------------------------------------
  587. ## @fn DESTROY
  588. # @brief Deconstructor
  589. sub DESTROY() {
  590. my ($self) = @_;
  591. if ($DEBUG) {
  592. print STDERR "Fetch::DESTROY()\n";
  593. }
  594. $self->{'cookies'} = undef();
  595. $self->{'ua'} = undef();
  596. }
  597. # Packages must return true
  598. return 1;