Fetch.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  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. }
  145. #----------------------------------------------------------------------
  146. ## @fn delay
  147. # @brief Set or get the Delay
  148. # @param delay
  149. # @return delay
  150. sub delay {
  151. my ($self, $delay) = @_;
  152. if ($DEBUG) {
  153. print STDERR "Fetch::delay()\n";
  154. }
  155. if (defined($delay)) {
  156. $delay = int($delay);
  157. if ($delay >= 0) {
  158. $self->{'delay'} = $delay;
  159. }
  160. }
  161. return $self->{'delay'};
  162. }
  163. #----------------------------------------------------------------------
  164. ## @fn Timeout
  165. # @brief Set or get the Timeout
  166. # @return timeout
  167. sub timeout {
  168. my ($self, $timeout) = @_;
  169. if ($DEBUG) {
  170. print STDERR "Fetch::timeout()\n";
  171. }
  172. if (defined($timeout)) {
  173. $timeout = int($timeout);
  174. if ($timeout > 0) {
  175. $self->{'timeout'} = $timeout;
  176. }
  177. }
  178. return $self->{'timeout'};
  179. }
  180. #----------------------------------------------------------------------
  181. ## @fn redial
  182. # @brief Set or get the Redial
  183. # @param redial
  184. # @return redial
  185. sub redial {
  186. my ($self, $redial) = @_;
  187. if ($DEBUG) {
  188. print STDERR "Fetch::redial()\n";
  189. }
  190. if (defined($redial)) {
  191. $redial = int($redial);
  192. if ($redial > 0) {
  193. $self->{'redial'} = $redial;
  194. }
  195. }
  196. return $self->{'redial'};
  197. }
  198. #----------------------------------------------------------------------
  199. ## @fn cookiefile
  200. # @brief Set or get the Cookie File
  201. # @param cookiefile
  202. # @return cookiefile
  203. sub cookiefile {
  204. my ($self, $cookiefile) = @_;
  205. if ($DEBUG) {
  206. print STDERR "Fetch::cookiefile()\n";
  207. }
  208. if (defined($cookiefile) && (!defined($self->{'cookiefile'}) || $cookiefile ne $self->{'cookiefile'})) {
  209. $self->{'cookiefile'} = $cookiefile;
  210. $self->init_ua();
  211. }
  212. return $self->{'cookiefile'};
  213. }
  214. #----------------------------------------------------------------------
  215. ## @fn uas
  216. # @brief Set or get the User Agent String
  217. # @param uas
  218. # @return uas
  219. sub uas {
  220. my ($self, $uas) = @_;
  221. if ($DEBUG) {
  222. print STDERR "Fetch::uas()\n";
  223. }
  224. if (defined($uas) && length($uas) && $uas ne $self->{'uas'}) {
  225. $self->{'uas'} = $uas;
  226. $self->Fetch::init_ua();
  227. }
  228. return $self->{'uas'};
  229. }
  230. #----------------------------------------------------------------------
  231. ## @fn headers
  232. # @brief Set or get the Headers
  233. # @param data
  234. # @return headers
  235. sub headers {
  236. my $self = shift(@_);
  237. my ($data) = @_;
  238. if ($DEBUG) {
  239. print STDERR "Fetch::headers()\n";
  240. }
  241. if (defined($data)) {
  242. if (reftype($data) eq 'HASH') {
  243. %{ $self->{'headers'} } = %{$data};
  244. } else {
  245. %{ $self->{'headers'} } = %{@_};
  246. }
  247. }
  248. return %{ $self->{'headers'} };
  249. }
  250. #----------------------------------------------------------------------
  251. ## @fn url
  252. # @brief Set or get the URL
  253. # @param url
  254. # @return url
  255. sub url {
  256. my ($self, $url) = @_;
  257. if ($DEBUG) {
  258. print STDERR "Fetch::url()\n";
  259. }
  260. if (defined($url)) {
  261. $self->{'referer'} = $self->{'url'};
  262. $self->{'url'} = $url;
  263. }
  264. return $self->{'url'};
  265. }
  266. #----------------------------------------------------------------------
  267. ## @fn file
  268. # @brief Set or get the file
  269. # @param file
  270. # @return file
  271. sub file {
  272. my ($self, $file) = @_;
  273. if ($DEBUG) {
  274. print STDERR "Fetch::file()\n";
  275. }
  276. if (defined($file)) {
  277. $self->{'file'} = $file;
  278. }
  279. return $self->{'file'};
  280. }
  281. #----------------------------------------------------------------------
  282. ## @fn post_content
  283. # @brief Set or get the POST content
  284. # @param data
  285. # @return post_content
  286. sub post_content {
  287. my ($self) = shift(@_);
  288. my ($data) = @_;
  289. if ($DEBUG) {
  290. print STDERR "Fetch::post_content()\n";
  291. }
  292. my $content = '';
  293. # Return immediately if there is no change
  294. if (!defined($data)) {
  295. return $self->{'post_content'};
  296. }
  297. # Set the content type
  298. $self->{'content_type'} = 'application/x-www-form-urlencoded';
  299. # Skip processing is the data is empty
  300. my $type = reftype($data);
  301. if (defined($type) || length($data) > 0) {
  302. # Convert the hash to a string
  303. if (defined($type) && $type eq 'HASH') {
  304. foreach my $key (keys(%{$data})) {
  305. if (defined($content) && length($content)) {
  306. $content .= '&';
  307. }
  308. # Ensure the value is always defined (even if empty)
  309. my $value = $data->{$key};
  310. if (!$value) {
  311. $value = '';
  312. }
  313. # URL encode if necessary
  314. if ($key =~ /[^\w\%]/) {
  315. $key = uri_escape($key);
  316. }
  317. if ($value =~ /[^\w\%]/) {
  318. $value = uri_escape($value);
  319. }
  320. # Append the string
  321. $content .= $key . '=' . $value;
  322. }
  323. } else {
  324. # No conversion needed for strings
  325. $content = $data;
  326. }
  327. }
  328. # Save and return
  329. $self->{'post_content'} = $content;
  330. if ($DEBUG) {
  331. print STDERR 'Fetch::post_content(' . length($content) . '): ' . $content . "\n";
  332. }
  333. return $self->{'post_content'};
  334. }
  335. #----------------------------------------------------------------------
  336. ## @fn post_content_multi
  337. # @brief Set or get the POST content multipart
  338. # @param data
  339. # @param boundary
  340. # @return post_content
  341. sub post_content_multi {
  342. my ($self, $data, $boundary) = @_;
  343. if ($DEBUG) {
  344. print STDERR "Fetch::post_content_multi()\n";
  345. }
  346. my $content = '';
  347. # Return immediately if there is no change
  348. if (!defined($data)) {
  349. return $self->{'post_content'};
  350. }
  351. # Pick a boundary if we need one
  352. if (!defined($boundary) || length($boundary) < 5) {
  353. $boundary = '';
  354. while (length($boundary) < 10) {
  355. $boundary .= chr(rand() * 127);
  356. $boundary =~ s/[^\w]//g;
  357. }
  358. }
  359. # Set the content type
  360. $self->{'content_type'} = 'multipart/form-data; boundary=' . $boundary;
  361. # Skip processing is the data is empty (or invalid)
  362. my $type = reftype($data);
  363. if (defined($type) && $type eq 'HASH') {
  364. # Convert the hash to a string
  365. foreach my $key (keys(%{$data})) {
  366. my $name = $key;
  367. if (ref($data->{$key}) ne 'ARRAY') {
  368. my @tmp = ($data->{$key});
  369. $data->{$key} = \@tmp;
  370. }
  371. foreach my $val (@{ $data->{$key} }) {
  372. $content .= '--' . $boundary . "\r\n";
  373. $content .= 'Content-Disposition: form-data; name="' . $name . "\"\r\n\r\n";
  374. $content .= $val . "\r\n";
  375. }
  376. }
  377. # End the multipart form
  378. $content .= '--' . $boundary . '--';
  379. }
  380. # Save and return
  381. $self->{'post_content'} = $content;
  382. if ($DEBUG) {
  383. print STDERR 'Fetch::post_content_multi(' . length($content) . "):\n" . $content . "\n";
  384. }
  385. return $self->{'post_content'};
  386. }
  387. #----------------------------------------------------------------------
  388. ## @fn status_code
  389. # @brief Get the Status Code
  390. # @return status_code
  391. sub status_code() {
  392. my ($self) = @_;
  393. if ($DEBUG) {
  394. print STDERR "Fetch::status_code()\n";
  395. }
  396. return $self->{'status_code'};
  397. }
  398. #----------------------------------------------------------------------
  399. ## @fn last_redirect
  400. # @brief Get the redirect URI
  401. # @return redirect_uri
  402. sub last_redirect() {
  403. my ($self) = @_;
  404. if ($DEBUG) {
  405. print STDERR "Fetch::redirect_uri()\n";
  406. }
  407. return $self->{'redirect_uri'};
  408. }
  409. #----------------------------------------------------------------------
  410. ## @fn fetch
  411. # @brief Performs a Fetch
  412. # @return status code
  413. sub fetch() {
  414. my ($self) = shift(@_);
  415. my (%params) = @_;
  416. if ($DEBUG) {
  417. print STDERR "Fetch::fetch()\n";
  418. }
  419. my ($request, $response, $host);
  420. # Save the URL and filename, if any
  421. $self->Fetch::url($params{'url'});
  422. $self->Fetch::file($params{'file'});
  423. # Decide if we care about the result code
  424. my $nocheck = 0;
  425. if ($params{'nocheck'}) {
  426. $nocheck = 1;
  427. }
  428. # Check the URL
  429. if (!length($self->Fetch::url())) {
  430. $self->{'status_code'} = 400;
  431. return $self->Fetch::status_code();
  432. }
  433. # Create a GET or POST request
  434. if (length($self->Fetch::post_content())) {
  435. $request = HTTP::Request->new('POST' => $self->Fetch::url());
  436. $request->header('Content-Type' => $self->{'content_type'});
  437. $request->header('Content-Length' => length($self->Fetch::post_content()));
  438. $request->content($self->Fetch::post_content());
  439. if ($DEBUG) {
  440. print STDERR 'Fetching via POST: ' . $self->Fetch::url() . "\n";
  441. }
  442. } else {
  443. $request = HTTP::Request->new('GET' => $self->Fetch::url());
  444. if ($DEBUG) {
  445. print STDERR 'Fetching via GET: ' . $self->Fetch::url() . "\n";
  446. }
  447. }
  448. # Always add the 'Host' header
  449. # But allow headers() to override the value
  450. if (!defined($self->{'headers'}{'Host'}) || length($self->{'headers'}{'Host'}) < 1) {
  451. my ($host) = $self->Fetch::url() =~ /\w+:\/\/([^\/]+)/;
  452. $request->header('Host' => $host);
  453. if ($DEBUG) {
  454. print STDERR 'Setting host header: ' . $host . "\n";
  455. }
  456. }
  457. # Always add the 'Referer' header
  458. # But allow headers() to override the value
  459. if (!defined($self->{'headers'}{'Referer'}) || length($self->{'headers'}{'Referer'}) < 1) {
  460. $request->header('Referer' => $self->{'referer'});
  461. if ($DEBUG) {
  462. print STDERR 'Setting referer header: ' . $self->{'referer'} . "\n";
  463. }
  464. }
  465. # Append any extra headers
  466. foreach my $key (keys(%{ $self->{'headers'} })) {
  467. $request->header($key => $self->{'headers'}{$key});
  468. if ($DEBUG) {
  469. print STDERR 'Setting header: ' . $key . ' => ' . $self->{'headers'}{$key} . "\n";
  470. }
  471. }
  472. # Dial until we get results
  473. my $connectcount = $self->Fetch::redial();
  474. GET: {
  475. # Limit redials
  476. if (!$connectcount) {
  477. last GET;
  478. }
  479. $connectcount--;
  480. # Run the request
  481. $self->{'ua'}->ZACH_clear_redirect();
  482. $response = $self->{'ua'}->request($request);
  483. $self->{'redirect_uri'} = $self->{'ua'}->ZACH_last_redirect();
  484. if (defined($self->{'cookies'})) {
  485. $self->{'cookies'}->extract_cookies($response);
  486. }
  487. # Check for errors, loop if we aren't happy
  488. if ($nocheck) {
  489. last GET;
  490. } elsif ($response->status_line =~ /^200/) {
  491. last GET;
  492. } else {
  493. sleep($self->Fetch::delay() * (rand(2) + 0.5));
  494. redo GET;
  495. }
  496. }
  497. # Grab the status code
  498. ($self->{'status_code'}) = $response->status_line =~ /^(\d+)\s/;
  499. # Grab the content for future use
  500. $self->{'content'} = $response->content();
  501. # Save the output (save() does its own validity checks)
  502. $self->Fetch::save('nocheck' => $nocheck);
  503. # Write any new cookies to disk
  504. if (defined($self->{'cookies'}) && ref($self->{'cookies'})) {
  505. $self->{'cookies'}->save();
  506. }
  507. # Return the status code
  508. return $self->Fetch::status_code();
  509. }
  510. #----------------------------------------------------------------------
  511. ## @fn content
  512. # @brief Get the Content
  513. # @return data
  514. sub content() {
  515. my ($self) = @_;
  516. if ($DEBUG) {
  517. print STDERR "Fetch::content()\n";
  518. }
  519. my $data = $self->{'content'};
  520. $data =~ s/\r\n/\n/;
  521. # Return an array when requested
  522. if (wantarray) {
  523. return split(/\n/, $data);
  524. }
  525. # Otherwise return a string
  526. return $data;
  527. }
  528. #----------------------------------------------------------------------
  529. ## @fn save
  530. # @brief Save
  531. # @param params
  532. # @return 1 if successful, 0 if not
  533. sub save() {
  534. my ($self) = shift(@_);
  535. if ($DEBUG) {
  536. print STDERR "Fetch::save()\n";
  537. }
  538. my (%params) = @_;
  539. # Optionally update the file name
  540. $self->Fetch::file($params{'file'});
  541. # Bail on bad HTTP result codes (if we're checking)
  542. if (!$params{'nocheck'}) {
  543. if ($self->Fetch::status_code() != 200) {
  544. print STDERR "Fetch::save: Refusing to save file with bad status code\n";
  545. return 1;
  546. }
  547. }
  548. # Bail on bad file names -- but don't die, because we don't always want to save
  549. if (!defined($self->Fetch::file()) || !length($self->Fetch::file())) {
  550. return 1;
  551. }
  552. # Bail on I/O errors
  553. if (!open(OUT, '>', $self->Fetch::file())) {
  554. die('Fetch::save: Unable to open output file: ' . $self->Fetch::file() . ': ' . $! . "\n");
  555. }
  556. # Print out the URL, the post content (if any)
  557. if (defined($self->{'print_comments'}) && $self->{'print_comments'}) {
  558. print OUT '<!-- ' . $self->Fetch::url() . ' -->' . "\n";
  559. if (length($self->Fetch::post_content())) {
  560. print OUT '<!-- ' . $self->Fetch::post_content() . ' -->' . "\n";
  561. }
  562. print OUT "\n\n";
  563. }
  564. # Print the document body
  565. print OUT $self->Fetch::content() . "\n";
  566. close(OUT);
  567. # Return 0 on success
  568. return 0;
  569. }
  570. #----------------------------------------------------------------------
  571. ## @fn DESTROY
  572. # @brief Deconstructor
  573. sub DESTROY() {
  574. my ($self) = @_;
  575. if ($DEBUG) {
  576. print STDERR "Fetch::DESTROY()\n";
  577. }
  578. $self->{'cookies'} = undef();
  579. $self->{'ua'} = undef();
  580. }
  581. # Packages must return true
  582. return 1;