File Coverage

blib/lib/Bot/Pastebot/Server/Http.pm
Criterion Covered Total %
statement 42 272 15.4
branch 0 110 0.0
condition 0 15 0.0
subroutine 14 22 63.6
pod 0 7 0.0
total 56 426 13.1


line stmt bran cond sub pod time code
1             # The web server portion of our program.
2              
3             package Bot::Pastebot::Server::Http;
4              
5 1     1   5 use warnings;
  1         3  
  1         31  
6 1     1   5 use strict;
  1         2  
  1         28  
7              
8 1     1   4 use Socket;
  1         3  
  1         734  
9 1     1   2590 use HTTP::Negotiate;
  1         4965  
  1         63  
10 1     1   3232 use HTTP::Response;
  1         23379  
  1         41  
11              
12 1     1   11 use POE::Session;
  1         2  
  1         13  
13 1     1   1304 use POE::Component::Server::TCP;
  1         5419  
  1         39  
14 1     1   1149 use POE::Filter::HTTPD;
  1         17361  
  1         47  
15 1     1   1228 use File::ShareDir qw(dist_dir);
  1         6510  
  1         82  
16              
17 1     1   20 use Bot::Pastebot::Conf qw( get_names_by_type get_items_by_name );
  1         2  
  1         53  
18 1         89 use Bot::Pastebot::WebUtil qw(
19             static_response parse_content parse_cookie dump_content html_encode
20             is_true cookie redirect
21 1     1   6 );
  1         2  
22 1     1   4 use Bot::Pastebot::Data qw( channels store_paste fetch_paste is_ignored );
  1         2  
  1         54  
23              
24 1     1   6900 use Perl::Tidy;
  1         239814  
  1         221  
25              
26             # Dumps the request to stderr.
27             sub DUMP_REQUEST () { 0 }
28              
29             sub WEB_SERVER_TYPE () { "web_server" }
30              
31             sub PAGE_FOOTER () {
32             (
33             "<div align=right><font size='-1'>" .
34             "<a href='http://sf.net/projects/pastebot/'>Pastebot</a>" .
35             " is powered by " .
36             "<a href='http://poe.perl.org/'>POE</a>.</font></div>"
37             )
38             }
39              
40             # Return this module's configuration.
41              
42 1     1   9 use Bot::Pastebot::Conf qw(SCALAR REQUIRED);
  1         2  
  1         3476  
43              
44             my %conf = (
45             web_server => {
46             name => SCALAR | REQUIRED,
47             iface => SCALAR,
48             ifname => SCALAR,
49             port => SCALAR | REQUIRED,
50             irc => SCALAR,
51             proxy => SCALAR,
52             iname => SCALAR,
53             static => SCALAR,
54             template => SCALAR,
55             },
56             );
57              
58 0     0 0   sub get_conf { return %conf }
59              
60             #------------------------------------------------------------------------------
61             # A web server.
62              
63             # Start an HTTPD session. Note that this handler receives both the
64             # local bind() address ($my_host) and the public server address
65             # ($my_ifname). It uses $my_ifname to build HTML that the outside
66             # world can see.
67              
68             sub httpd_session_started {
69             my (
70 0     0 0   $heap,
71             $socket, $remote_address, $remote_port,
72             $my_name, $my_host, $my_port, $my_ifname, $my_isrv,
73             $proxy, $my_iname, $my_template, $my_static,
74             ) = @_[HEAP, ARG0..$#_];
75              
76             # TODO: I think $my_host is obsolete. Maybe it can be removed, and
77             # $my_ifname can be used exclusively?
78              
79 0           $heap->{my_host} = $my_host;
80 0           $heap->{my_port} = $my_port;
81 0           $heap->{my_name} = $my_name;
82 0           $heap->{my_inam} = $my_ifname;
83 0           $heap->{my_iname} = $my_iname;
84 0           $heap->{my_isrv} = $my_isrv;
85 0           $heap->{my_proxy} = $proxy;
86 0           $heap->{my_static} = $my_static;
87 0           $heap->{my_template} = $my_template;
88              
89              
90 0           $heap->{remote_addr} = inet_ntoa($remote_address);
91 0           $heap->{remote_port} = $remote_port;
92              
93 0           $heap->{wheel} = new POE::Wheel::ReadWrite(
94             Handle => $socket,
95             Driver => new POE::Driver::SysRW,
96             Filter => new POE::Filter::HTTPD,
97             InputEvent => 'got_query',
98             FlushedEvent => 'got_flush',
99             ErrorEvent => 'got_error',
100             );
101             }
102              
103             # An HTTPD response has flushed. Stop the session.
104             sub httpd_session_flushed {
105 0     0 0   delete $_[HEAP]->{wheel};
106             }
107              
108             # An HTTPD session received an error. Stop the session.
109             sub httpd_session_got_error {
110 0     0 0   my ($session, $heap, $operation, $errnum, $errstr) = @_[
111             SESSION, HEAP, ARG0, ARG1, ARG2
112             ];
113 0           warn(
114             "connection session ", $session->ID,
115             " got $operation error $errnum: $errstr\n"
116             );
117 0           delete $heap->{wheel};
118             }
119              
120             # Process HTTP requests.
121             sub httpd_session_got_query {
122 0     0 0   my ($kernel, $heap, $request) = @_[KERNEL, HEAP, ARG0];
123              
124             ### Log the request.
125              
126             # Space-separated list:
127             # Remote address (client address)
128             # -
129             # -
130             # [GMT date in brackets: DD/Mon/CCYY:HH:MM:SS -0000]
131             # "GET url HTTP/x.y" <-- in quotes
132             # response code
133             # response size
134             # referer
135             # user-agent string
136              
137             ### Responded with an error. Send it directly.
138              
139 0 0         if ($request->isa("HTTP::Response")) {
140 0           $heap->{wheel}->put($request);
141 0           return;
142             }
143              
144             ### These requests don't require authentication.
145              
146 0           my $url = $request->url() . '';
147              
148             # strip multiple // to prevent errors
149 0           $url =~ s,//+,/,;
150              
151             # simple url decode
152 0           $url =~ s,%([[:xdigit:]]{2}),chr hex $1,eg;
  0            
153              
154             ### Fetch the highlighted style sheet.
155              
156 0 0         if ($url eq '/style') {
157 0           my $response = static_response(
158             $heap->{my_template}, "$heap->{my_static}/highlights.css", { }
159             );
160 0           $heap->{wheel}->put( $response );
161 0           return;
162             }
163              
164             ### Fetch some kind of data.
165              
166 0 0         if ($url =~ m{^/static/(.+?)\s*$}) {
167             # TODO - Better path support?
168 0           my $filename = $1;
169 0           $filename =~ s{/\.+}{/}g; # Remove ., .., ..., etc.
170 0           $filename =~ s{/+}{/}g; # Combine // into /
171 0           $filename = "$heap->{my_static}/$filename";
172              
173 0           my ($code, $type, $content);
174              
175 0 0         if (-e $filename) {
176 0 0         if (open(FILE, "<$filename")) {
177 0           $code = 200;
178 0           local $/;
179 0           $content = <FILE>;
180 0           close FILE;
181              
182             # TODO - Better type support.
183 0 0         if ($filename =~ /\.(gif|jpe?g|png)$/i) {
184 0           $type = lc($1);
185 0 0         $type = "jpeg" if $type eq "jpg";
186 0           $type = "image/$1";
187             }
188             }
189             else {
190 0           $code = 500;
191 0           $type = "text/html";
192 0           $content = (
193             "<html><head><title>File Error</title></head>" .
194             "<body>Error opening $filename: $!</body></html>"
195             );
196             }
197             }
198             else {
199 0           $code = 404;
200 0           $type = "text/html";
201 0           $content = (
202             "<html><head><title>404 File Not Found</title></head>" .
203             "<body>File $filename does not exist.</body></html>"
204             );
205             }
206              
207 0           my $response = HTTP::Response->new($code);
208 0           $response->push_header('Content-type', $type);
209 0           $response->content($content);
210 0           $heap->{wheel}->put( $response );
211 0           return;
212             }
213              
214             ### Store paste.
215              
216 0 0         if ($url =~ m,/paste$,) {
217 0           my $content = parse_content($request->content());
218              
219 0 0 0       if (defined $content->{paste} and length $content->{paste}) {
220 0           my $channel = $content->{channel};
221 0 0         defined $channel or $channel = "";
222 0           $channel =~ tr[\x00-\x1F\x7F][]d;
223              
224 0           my $remote_addr = $heap->{remote_addr};
225 0 0 0       if ($heap->{my_proxy} && $remote_addr eq $heap->{my_proxy}) {
226             # apache sets the X-Forwarded-For header to a list of the
227             # IP addresses that were forwarded from/to
228 0           my $forwarded = $request->headers->header('X-Forwarded-For');
229 0 0         if ($forwarded) {
230 0           ($remote_addr) = $forwarded =~ /([^,\s]+)$/;
231             }
232             # else must be local?
233             }
234              
235 0           my $error = "";
236 0 0         if (length $channel) {
237             # See if it matches.
238 0 0         if (is_ignored($heap->{my_isrv}, $channel, $remote_addr)) {
239 0           $error = (
240             "<p><b><font size='+1' color='#800000'>" .
241             "Your IP address has been blocked from pasting to $channel." .
242             "</font></b></p>"
243             );
244 0           $channel = "";
245             }
246             }
247              
248             # Goes as a separate block.
249 0 0         if (length $channel) {
250 0 0         unless (grep $_ eq $channel, channels($heap->{my_isrv})) {
251 0           $error = (
252             "<p><b><font size='+1' color='#800000'>" .
253             "I'm not on $channel." .
254             "</font></b></p>"
255             );
256 0           $channel = "";
257             }
258             }
259              
260 0           my $nick = $content->{nick};
261 0 0         $nick = "" unless defined $nick;
262 0           $nick =~ tr[\x00-\x1F\x7F][ ]s;
263 0           $nick =~ s/\s+/ /g;
264 0           $nick =~ s/^\s+//;
265 0           $nick =~ s/\s+$//;
266 0           $nick = html_encode($nick);
267              
268 0 0         if (length $nick) {
269 0           $nick = qq("$nick");
270             }
271             else {
272 0           $nick = "Someone";
273             }
274              
275 0           $nick .= " at $remote_addr";
276              
277             # <CanyonMan> how about adding a form field with a "Subject"
278             # line ?
279              
280 0           my $summary = $content->{summary};
281 0 0         $summary = "" unless defined $summary;
282 0           $summary =~ tr[\x00-\x1F\x7F][ ]s;
283 0           $summary =~ s/\s+/ /g;
284 0           $summary =~ s/^\s+//;
285 0           $summary =~ s/\s+$//;
286              
287             # <TorgoX> [...] in the absence of anything in the subject, it
288             # falls back to [the first 30 characters of what's pasted]
289              
290 0           my $paste = $content->{paste};
291 0 0         unless (length($summary)) {
292 0           $summary = $paste;
293 0           $summary =~ s/\s+/ /g;
294 0           $summary =~ s/^\s+//;
295 0           $summary = substr($summary, 0, 30);
296 0           $summary =~ s/\s+$//;
297             }
298              
299 0 0         $summary = "something" unless length $summary;
300 0           my $html_summary = html_encode($summary);
301              
302 0           my $id = store_paste(
303             $nick, $html_summary, $paste,
304             $heap->{my_isrv}, $channel, $remote_addr
305             );
306 0           my $paste_link;
307 0 0         if (defined $heap->{my_iname}) {
308 0 0         $paste_link = (
309             $heap->{my_iname} .
310             (
311             ($heap->{my_iname} =~ m,/$,)
312             ? $id
313             : "/$id"
314             )
315             );
316             }
317             else {
318 0           $paste_link = "http://$heap->{my_inam}:$heap->{my_port}/$id";
319             }
320              
321             # show number of lines in paste in channel announce
322 0           my $paste_lines = 0;
323 0           $paste_lines++ for $paste =~ m/^.*$/mg;
324              
325 0           $paste = fix_paste($paste, 0, 0, 0, 0);
326              
327 0           my $response;
328              
329 0 0         if( $error ) {
330 0           $response = static_response(
331             $heap->{my_template},
332             "$heap->{my_static}/paste-error.html",
333             {
334             error => $error,
335             footer => PAGE_FOOTER,
336             }
337             );
338             } else {
339 0           $response = redirect(
340             $heap->{my_template},
341             "$heap->{my_static}/paste-answer.html",
342             {
343             paste_id => $id,
344             paste_link => $paste_link,
345             },
346             );
347             }
348              
349 0 0 0       if ($channel and $channel =~ /^\#/) {
350 0 0         $kernel->post(
351             "irc_client_$heap->{my_isrv}" => announce =>
352             $channel,
353             "$nick pasted \"$summary\" ($paste_lines line" .
354             ($paste_lines == 1 ? '' : 's') . ") at $paste_link"
355             );
356             }
357             else {
358 0           warn "channel $channel was strange";
359             }
360              
361 0           $heap->{wheel}->put( $response );
362 0           return;
363             }
364              
365             # Error goes here.
366             }
367              
368             ### Fetch paste.
369              
370 0 0         if ($url =~ m{^/(\d+)(?:\?(.*?)\s*)?$}) {
371 0           my ($num, $params) = ($1, $2);
372 0           my ($nick, $summary, $paste) = fetch_paste($num);
373              
374 0 0         if (defined $paste) {
375 0           my @flag_names = qw(ln tidy hl wr);
376 0           my $cookie = parse_cookie($request->headers->header('Cookie'));
377 0           my $query = parse_content($params);
378              
379             ### Make the paste pretty.
380              
381 0           my $store = is_true($query->{store});
382 0           my %flags;
383 0           for my $flag (@flag_names) {
384 0 0 0       $flags{$flag} = $store || exists $query->{$flag}
385             ? is_true( $query->{$flag})
386             : is_true($cookie->{$flag});
387             }
388              
389 0           my $tx = is_true($query->{tx});
390              
391 0           my $variants = [
392             ['html', 1.000, 'text/html', undef, 'us-ascii', 'en', undef],
393             ['text', 0.950, 'text/plain', undef, 'us-ascii', 'en', undef],
394             ];
395 0           my $choice = choose($variants, $request);
396 0 0 0       $tx = 1 if $choice && $choice eq 'text';
397              
398 0 0         $paste = fix_paste($paste, @flags{@flag_names}) unless $tx;
399              
400             # Spew the paste.
401              
402 0           my $response;
403 0 0         if ($tx) {
404 0           $response = HTTP::Response->new(200);
405 0           $response->push_header( 'Content-type', 'text/plain' );
406 0           $response->content($paste);
407             }
408             else {
409 0 0         $response = static_response(
410             $heap->{my_template},
411             "$heap->{my_static}/paste-lookup.html",
412             { bot_name => $heap->{my_name},
413             paste_id => $num,
414             nick => $nick,
415             summary => $summary,
416             paste => $paste,
417             footer => PAGE_FOOTER,
418             tx => ( $tx ? "checked" : "" ),
419 0 0         map { $_ => $flags{$_} ? "checked" : "" } @flag_names,
420             }
421             );
422 0 0         if ($store) {
423 0           for my $flag (@flag_names) {
424 0           $response->push_header('Set-Cookie' => cookie($flag => $flags{$flag}, $request));
425             }
426             }
427             }
428              
429 0           $heap->{wheel}->put( $response );
430 0           return;
431             }
432              
433 0           my $response = HTTP::Response->new(404);
434 0           $response->push_header( 'Content-type', 'text/html; charset=utf-8' );
435 0           $response->content(
436             "<html>" .
437             "<head><title>Paste Not Found</title></head>" .
438             "<body><p>Paste not found.</p></body>" .
439             "</html>"
440             );
441 0           $heap->{wheel}->put( $response );
442 0           return;
443             }
444              
445             ### Root page.
446              
447             # 2003-12-22 - RC - Added _ and - as legal characters for channel
448             # names. What else?
449 0 0         if ($url =~ m!^/([\#\-\w\.]+)?!) {
450              
451             # set default channel from request URL, if possible
452 0           my $prefchan = $1;
453 0 0         if (defined $prefchan) {
454 0 0         $prefchan = "#$prefchan" unless $prefchan =~ m,^\#,;
455             }
456             else {
457 0           $prefchan = '';
458             }
459              
460             # Dynamically build the channel options from the configuration
461             # file's list.
462 0           my @channels = channels($heap->{my_isrv});
463 0           unshift @channels, '';
464              
465 0 0         @channels = map {
    0          
466 0           qq(<option value="$_")
467             . ($_ eq $prefchan ? ' selected' : '')
468             . '>'
469             . ($_ eq '' ? '(none)' : $_)
470             . '</option>'
471             } sort @channels;
472              
473             # Build content.
474              
475 0           my $iname = $heap->{my_iname};
476 0 0         $iname .= '/' unless $iname =~ m#/$#;
477 0           my $response = static_response(
478             $heap->{my_template},
479             "$heap->{my_static}/paste-form.html",
480             { bot_name => $heap->{my_name},
481             channels => "@channels",
482             footer => PAGE_FOOTER,
483             iname => $iname,
484             }
485             );
486 0           $heap->{wheel}->put($response);
487 0           return;
488             }
489              
490             ### Default handler dumps everything it can about the request.
491              
492 0           my $response = HTTP::Response->new( 200 );
493 0           $response->push_header( 'Content-type', 'text/html' );
494              
495             # Many of the headers dumped here are undef. We turn off warnings
496             # here so the program doesn't constantly squeal.
497              
498 0           local $^W = 0;
499              
500 0           $response->content(
501             "<html><head><title>Strange Request Dump</title></head>" .
502             "<body>" .
503             "<p>" .
504             "Your request was strange. " .
505             "Here is everything I could figure out about it:" .
506             "</p>" .
507             "<table border=1>" .
508              
509             join(
510             "",
511             map {
512 0           "<tr><td><header></td><td>" . $request->$_() . "</td></tr>"
513             } qw(
514             authorization authorization_basic content_encoding
515             content_language content_length content_type content date
516             expires from if_modified_since if_unmodified_since
517             last_modified method protocol proxy_authorization
518             proxy_authorization_basic referer server title url user_agent
519             www_authenticate
520             )
521             ) .
522              
523             join(
524             "",
525             map {
526 0           "<tr><td><header></td><td>" . $request->header($_) . "</td></tr>"
527             } qw(
528             Accept Connection Host
529             username opaque stale algorithm realm uri qop auth nonce
530             cnonce nc response
531             )
532             ) .
533              
534             "</table>" .
535              
536             dump_content($request->content()) .
537              
538             "<p>Request as string=" . $request->as_string() . "</p>" .
539              
540             "</body></html>"
541             );
542              
543             # A little debugging here.
544 0           if (DUMP_REQUEST) {
545             my $request_as_string = $request->as_string();
546             warn unpack('H*', $request_as_string), "\n";
547             warn "Request has CR.\n" if $request_as_string =~ /\x0D/;
548             warn "Request has LF.\n" if $request_as_string =~ /\x0A/;
549             }
550              
551 0           $heap->{wheel}->put( $response );
552 0           return;
553             }
554              
555             # Start the HTTPD server.
556              
557             sub initialize {
558 0     0 0   foreach my $server (get_names_by_type(WEB_SERVER_TYPE)) {
559 0           my %conf = get_items_by_name($server);
560 0           my %ircconf = get_items_by_name($conf{irc});
561              
562 0           my $static = $conf{static};
563 0 0         unless (defined $static) {
564 0           $static = dist_dir("Bot-Pastebot");
565             }
566              
567              
568 0           my $template;
569 0 0         if (defined $conf{template}) {
570 0           my $template_class = $conf{template};
571 0           my $filename = $template_class;
572 0           $filename =~ s[::][/]g;
573              
574 0           eval { require "$filename.pm" };
  0            
575 0 0         die("Unable to load template class '$template_class': $@") if $@;
576              
577 0           $template = $template_class->new();
578 0 0         die("Unable to instantiate template object.\n") unless $template;
579              
580             } else {
581 0           require Bot::Pastebot::TextTemplate;
582 0 0         $template = Bot::Pastebot::TextTemplate->new()
583             or die("Unable to instantiate default template object.\n");
584             }
585              
586              
587             POE::Component::Server::TCP->new(
588             Port => $conf{port},
589             (
590             (defined $conf{iface})
591             ? ( Address => $conf{iface} )
592             : ()
593             ),
594             # TODO - Can we use the discrete callbacks?
595             Acceptor => sub {
596 0     0     POE::Session->create(
597             inline_states => {
598             _start => \&httpd_session_started,
599             got_flush => \&httpd_session_flushed,
600             got_query => \&httpd_session_got_query,
601             got_error => \&httpd_session_got_error,
602             },
603              
604             # Note the use of ifname here in ARG6. This gives the
605             # responding session knowledge of its host name for
606             # building HTML responses. Most of the time it will be
607             # identical to iface, but sometimes there may be a reverse
608             # proxy, firewall, or NATD between the address we bind to
609             # and the one people connect to. In that case, ifname is
610             # the address the outside world sees, and iface is the one
611             # we've bound to.
612              
613             args => [
614             @_[ARG0..ARG2], $server,
615             $conf{iface}, $conf{port}, $conf{ifname}, $conf{irc},
616             $conf{proxy}, $conf{iname}, $template, $static
617             ],
618             );
619             },
620 0 0         );
621             }
622             }
623              
624             ### Fix paste for presentability.
625              
626             sub fix_paste {
627 0     0 0   my ($paste, $line_nums, $tidied, $highlighted, $wrapped) = @_;
628              
629             ### If the code is tidied, then tidy it.
630              
631 0 0         if ($tidied) {
632 0           my $tidy_version = "";
633 0           eval {
634 0           Perl::Tidy::perltidy(
635             source => \$paste,
636             destination => \$tidy_version,
637             argv => [ '-q', '-nanl', '-fnl' ],
638             );
639             };
640 0 0         if ($@) {
641 0           $paste = "Could not tidy this paste (try turning tidying off): $@";
642             }
643             else {
644 0           $paste = $tidy_version;
645             }
646             }
647              
648             ### If the code is to be highlighted, then highlight it.
649              
650 0 0         if ($highlighted) {
651 0           my @html_args = qw( -q -html -pre );
652 0 0         push @html_args, "-nnn" if $line_nums;
653              
654 0           my $highlighted = "";
655 0           eval {
656 0           Perl::Tidy::perltidy(
657             source => \$paste,
658             destination => \$highlighted,
659             argv => \@html_args,
660             );
661             };
662 0 0         if ($@) {
663 0           $highlighted = (
664             "Could not highlight the paste (try turning highlighting off): $@"
665             );
666             }
667 0           return $highlighted;
668             }
669              
670             ### Code's not highlighted. HTML escaping time. Forgive me.
671              
672             # Prepend line numbers to each line.
673              
674 0 0         if ($line_nums) {
675 0           my $total_lines = 0;
676 0           $total_lines++ while ($paste =~ m/^/gm);
677 0           my $line_number_width = length($total_lines);
678 0 0         $line_number_width = 4 if $line_number_width < 4; # To match Perl::Tidy.
679              
680 0           my $line_number = 0;
681 0           while ($paste =~ m/^/gm) {
682 0           my $pos = pos($paste);
683 0           substr($paste, pos($paste), 0) = sprintf(
684             "\%${line_number_width}d ", ++$line_number
685             );
686 0           pos($paste) = $pos + 1;
687             }
688             }
689              
690 0           $paste = html_encode($paste);
691              
692             # Normalize newlines. Translate whichever format to just \n, and
693             # limit the number of consecutive newlines to two.
694              
695 0           $paste =~ s/(\x0d\x0a?|\x0a\x0d?)/\n/g;
696 0           $paste =~ s/\n\n+/\n\n/;
697              
698             # Buhbye.
699              
700 0 0         unless ($wrapped) {
701 0           substr($paste, 0, 0) = "<pre>";
702 0           $paste .= "</pre>";
703             }
704              
705 0           return $paste;
706             }
707              
708             1;