File Coverage

blib/lib/POE/Filter/SimpleHTTP.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package POE::Filter::SimpleHTTP;
2             our $VERSION = '0.091710';
3              
4 3     3   187331 use 5.010;
  3         13  
  3         113  
5 3     3   1578 use Moose;
  0            
  0            
6             extends('Exporter', 'Moose::Object');
7              
8             use Moose::Util::TypeConstraints;
9              
10             use Scalar::Util('blessed', 'reftype');
11              
12             use HTTP::Status;
13             use HTTP::Response;
14             use HTTP::Request;
15             use URI;
16             use Compress::Zlib;
17              
18             use POE::Filter::SimpleHTTP::Regex;
19             use POE::Filter::SimpleHTTP::Error;
20              
21             use UNIVERSAL::isa;
22              
23             use bytes;
24              
25             our @EXPORT = qw/PFSH_CLIENT PFSH_SERVER/;
26             our $DEBUG = 0;
27              
28             use constant
29             {
30             PARSE_START => 0,
31             PREAMBLE_COMPLETE => 1,
32             HEADER_COMPLETE => 2,
33             CONTENT_COMPLETE => 3,
34             PFSH_CLIENT => 0,
35             PFSH_SERVER => 1,
36             };
37              
38             subtype 'ParseState'
39             => as 'Int'
40             => where { -1 < $_ && $_ < 4 }
41             => message { 'Incorrect ParseState' };
42              
43             subtype 'FilterMode'
44             => as 'Int'
45             => where { $_ == 0 || $_ == 1 }
46             => message { 'Incorrect FilterMode' };
47              
48             subtype 'Uri'
49             => as 'Str'
50             => where { /$POE::Filter::SimpleHTTP::Regex::URI/ }
51             => message { 'Invalid URI string' };
52              
53             subtype 'HttpStatus'
54             => as 'Int'
55             => where { is_info($_) || is_success($_) || is_redirect($_) || is_error($_) }
56             => message { 'Invalid HTTP status code'};
57              
58             subtype 'HttpProtocol'
59             => as 'Str'
60             => where { /$POE::Filter::SimpleHTTP::Regex::PROTOCOL/ }
61             => message { 'Invalid HTTP protocol string' };
62              
63             subtype 'HttpMethod'
64             => as 'Str'
65             => where { /$POE::Filter::SimpleHTTP::Regex::METHOD/ }
66             => message { 'Invalid HTTP method' };
67              
68             has raw =>
69             (
70             is => 'rw',
71             isa => 'ArrayRef[Str]',
72             default => sub {[]},
73             clearer => 'clear_raw',
74             lazy => 1
75             );
76              
77             has preamble =>
78             (
79             is => 'rw',
80             isa => 'ArrayRef[Str]',
81             default => sub {[]},
82             clearer => 'clear_preamble',
83             lazy => 1
84             );
85              
86             has header =>
87             (
88             is => 'rw',
89             isa => 'ArrayRef[Str]',
90             default => sub {[]},
91             clearer => 'clear_header',
92             lazy => 1
93             );
94              
95             has content =>
96             (
97             is => 'rw',
98             isa => 'ArrayRef[Str]',
99             default => sub {[]},
100             clearer => 'clear_content',
101             lazy => 1
102             );
103              
104             has state =>
105             (
106             is => 'rw',
107             isa => 'ParseState',
108             default => 0,
109             clearer => 'clear_state',
110             lazy => 1
111             );
112              
113             has mode =>
114             (
115             is => 'rw',
116             isa => 'FilterMode',
117             default => 0,
118             lazy => 1
119             );
120              
121             has uri =>
122             (
123             is => 'rw',
124             isa => 'Uri',
125             default => '/',
126             lazy => 1
127             );
128              
129             has useragent =>
130             (
131             is => 'rw',
132             isa => 'Str',
133             default => __PACKAGE__ . '/' . $VERSION,
134             lazy => 1
135             );
136              
137             has host =>
138             (
139             is => 'rw',
140             isa => 'Str',
141             default => 'localhost',
142             lazy => 1
143             );
144              
145             has server =>
146             (
147             is => 'rw',
148             isa => 'Str',
149             default => __PACKAGE__ . '/' . $VERSION,
150             lazy => 1
151             );
152              
153             has mimetype =>
154             (
155             is => 'rw',
156             isa => 'Str',
157             default => 'text/plain',
158             lazy => 1
159             );
160              
161             has status =>
162             (
163             is => 'rw',
164             isa => 'HttpStatus',
165             default => 200,
166             lazy => 1
167             );
168              
169             has protocol =>
170             (
171             is => 'rw',
172             isa => 'HttpProtocol',
173             default => 'HTTP/1.1',
174             lazy => 1
175             );
176              
177             has 'method' =>
178             (
179             is => 'rw',
180             isa => 'HttpMethod',
181             default => 'GET',
182             lazy => 1
183             );
184              
185             sub clone()
186             {
187             my ($self, %params) = @_;
188             return $self->meta->clone_object($self, %params);
189             }
190              
191             sub isa()
192             {
193             my ($self, $arg) = (shift(@_), shift(@_));
194             if($arg eq 'POE::Filter')
195             {
196             return 1;
197             }
198             else
199             {
200             return $self->SUPER::isa($arg);
201             }
202             }
203              
204             sub reset()
205             {
206             my ($self) = @_;
207             $self->clear_raw();
208             $self->clear_preamble();
209             $self->clear_header();
210             $self->clear_content();
211             $self->clear_state();
212             }
213              
214             sub get_one()
215             {
216             my ($self) = @_;
217            
218             my $buffer = '';
219              
220             while(defined(my $raw = shift(@{$self->raw()})) || length($buffer))
221             {
222             $buffer .= $raw if defined($raw);
223             my $state = $self->state();
224              
225              
226             if($state < +PREAMBLE_COMPLETE)
227             {
228             if($buffer =~ /^\x0D\x0A/)
229             {
230             # skip the blank lines at the beginning if we have them
231             substr($buffer, 0, 2, '');
232             next;
233             }
234            
235             if($buffer =~ $POE::Filter::SimpleHTTP::Regex::REQUEST
236             or $buffer =~ $POE::Filter::SimpleHTTP::Regex::RESPONSE)
237             {
238             push(@{$self->preamble()}, $self->get_chunk(\$buffer));
239             $self->state(+PREAMBLE_COMPLETE);
240              
241             } else {
242            
243             return
244             [
245             POE::Filter::SimpleHTTP::Error->new
246             (
247             {
248             error => +UNPARSABLE_PREAMBLE,
249             context => $buffer
250             }
251             )
252             ];
253             }
254              
255             } elsif($state < +HEADER_COMPLETE) {
256            
257             if($buffer =~ /^\x0D\x0A/)
258             {
259             substr($buffer, 0, 2, '');
260             $self->state(+HEADER_COMPLETE);
261            
262             } else {
263            
264             #gather all of the headers from this chunk
265             while($buffer =~ $POE::Filter::SimpleHTTP::Regex::HEADER
266             and $buffer !~ /^\x0D\x0A/)
267             {
268             push(@{$self->header()}, $self->get_chunk(\$buffer));
269             }
270              
271             }
272              
273             } elsif($state < +CONTENT_COMPLETE) {
274            
275             if($buffer =~ /^\x0D\x0A/)
276             {
277             substr($buffer, 0, 2, '');
278             $self->state(+CONTENT_COMPLETE);
279              
280             } else {
281            
282             push(@{$self->content}, $self->get_chunk(\$buffer));
283             }
284              
285             if(!@{$self->raw} && !length($buffer))
286             {
287             $self->state(+CONTENT_COMPLETE);
288             }
289              
290             } else {
291            
292             if($buffer =~ /^\x0D\x0A$/)
293             {
294             # skip the blank lines at the end if we have them
295             substr($buffer, 0, 2, '');
296             next;
297             }
298              
299             return
300             [
301             POE::Filter::SimpleHTTP::Error->new
302             (
303             {
304             error => +TRAILING_DATA,
305             context => $buffer
306             }
307             )
308             ];
309             }
310             }
311            
312             if($self->state() == +CONTENT_COMPLETE)
313             {
314             my $ret = [$self->build_message()];
315             $self->reset();
316             return $ret;
317             }
318             else
319             {
320             warn Dumper($self) if $DEBUG;
321             return [];
322             }
323             };
324              
325             sub get_one_start()
326             {
327             my ($self, $data) = @_;
328            
329             if(!ref($data))
330             {
331             $data = [$data];
332             }
333              
334             push(@{$self->raw()}, @$data);
335            
336             };
337              
338             sub put()
339             {
340             my ($self, $content) = @_;
341            
342             my $ret = [];
343              
344             while(@$content)
345             {
346             my $check = shift(@$content);
347              
348             if(blessed($check) && $check->isa('HTTP::Message'))
349             {
350             push(@$ret, $check);
351             next;
352             }
353              
354             unshift(@$content, $check);
355              
356             my $http;
357              
358             if($self->mode() == +PFSH_SERVER)
359             {
360             my $response;
361            
362             $response = HTTP::Response->new($self->status());
363             $response->content_type($self->mimetype());
364             $response->server($self->server());
365            
366             while(@$content)
367             {
368             $response->add_content(shift(@$content));
369             }
370              
371             $http = $response;
372              
373             } else {
374              
375             my $request = HTTP::Request->new();
376              
377             $request->method($self->method());
378             $request->uri($self->uri());
379             $request->user_agent($self->useragent());
380             $request->content_type($self->mimetype());
381              
382             while(@$content)
383             {
384             $request->add_content(shift(@$content));
385             }
386            
387             $http = $request;
388             }
389              
390             $http->protocol($self->protocol());
391             push(@$ret, $http);
392             }
393              
394             return $ret;
395             };
396              
397              
398             sub get_chunk()
399             {
400             my ($self, $buffer) = @_;
401              
402             #find the break
403             my $break = index($$buffer, "\x0D\x0A");
404            
405             my $match;
406              
407             if($break < 0)
408             {
409             #pullout the whole string
410             $match = substr($$buffer, 0, length($$buffer), '');
411            
412             } elsif($break > -1) {
413            
414             #pull out string until newline
415             $match = substr($$buffer, 0, $break, '');
416            
417             #remove the CRLF from the buffer
418             substr($$buffer, 0, 2, '');
419             }
420              
421             return $match;
422             }
423              
424             sub build_message()
425             {
426             my ($self) = @_;
427            
428             my $message;
429              
430             my $preamble = shift(@{$self->preamble()});
431              
432             if($preamble =~ $POE::Filter::SimpleHTTP::Regex::REQUEST)
433             {
434             my ($method, $uri) = ($1, $2);
435              
436             $message = HTTP::Request->new($method, $uri);
437            
438             } elsif($preamble =~ $POE::Filter::SimpleHTTP::Regex::RESPONSE) {
439            
440             my ($code, $text) = ($2, $3);
441              
442             $message = HTTP::Response->new($code, $text);
443             }
444              
445              
446             foreach my $line (@{$self->header()})
447             {
448             if($line =~ $POE::Filter::SimpleHTTP::Regex::HEADER)
449             {
450             $message->header($1, $2);
451             }
452             }
453              
454             # If we have a transfer encoding, we need to decode it
455             # (ie. unchunkify, decompress, etc)
456             if($message->header('Transfer-Encoding'))
457             {
458             warn 'INSIDE TE' if $DEBUG;
459             my $te_raw = $message->header('Transfer-Encoding');
460             my $te_s =
461             [
462             (
463             map
464             {
465             my ($token) = split(/;/, $_); $token;
466             }
467             (reverse(split(/,/, $te_raw)))
468             )
469             ];
470            
471             my $buffer = '';
472             my $subbuff = '';
473             my $size = 0;
474             my $content = '';
475             $DB::single=1;
476             while(defined(my $content_line = shift(@{$self->content()})) )
477             {
478             # Start of a new chunk
479             if($size == 0)
480             {
481             if($content_line =~ /^([\dA-Fa-f]+)(?:\x0D\x0A)*/)
482             {
483             warn "CHUNK SIZE IN HEX: $1" if $DEBUG;
484             $size = hex($1);
485             }
486            
487             # If we got a zero size, it means time to process trailing
488             # headers if enabled
489             if($size == 0)
490             {
491             warn "SIZE ZERO HIT" if $DEBUG;
492             if($message->header('Trailer'))
493             {
494             while( my $tline = shift(@{$self->content()}) )
495             {
496             if($tline =~ $POE::Filter::SimpleHTTP::Regex::HEADER)
497             {
498             my ($key, $value) = ($1, $2);
499             $message->header($key, $value);
500             }
501             }
502             }
503             return $message;
504             }
505             }
506            
507             while($size > 0)
508             {
509             warn "SIZE: $size" if $DEBUG;
510             my $subline = shift(@{$self->content()});
511             while(length($subline))
512             {
513             warn 'LENGTH OF SUBLINE: ' . length($subline) if $DEBUG;
514             my $buff = substr($subline, 0, 4069, '');
515             $size -= length($buff);
516             $subbuff .= $buff;
517             }
518             }
519              
520             $buffer .= $subbuff;
521             warn 'BUFFER LENGTH: ' .length($buffer) if $DEBUG;
522              
523             $subbuff = '';
524             }
525            
526             my $chunk = shift(@$te_s);
527             if($chunk !~ /chunked/)
528             {
529             warn 'CHUNKED ISNT LAST' if $DEBUG;
530            
531             return POE::Filter::SimpleHTTP::Error->new
532             (
533             {
534             error => +CHUNKED_ISNT_LAST,
535             context => join(' ',($chunk, @$te_s))
536             }
537             );
538             }
539            
540             if(!scalar(@$te_s))
541             {
542             $content = $buffer;
543             }
544              
545             foreach my $te (@$te_s)
546             {
547             if($te =~ /deflate/)
548             {
549             my ($inflate, $status) = Compress::Zlib::inflateInit();
550             if(!defined($inflate))
551             {
552             warn 'INFLATE FAILED TO INIT' if $DEBUG;
553             return POE::Filter::SimpleHTTP::Error->new
554             (
555             {
556             error => +INFLATE_FAILED_INIT,
557             context => $status
558             }
559             );
560             }
561             else
562             {
563             warn 'BUFFER LENGTH BEFORE INFLATE: '. length($buffer) if $DEBUG;
564             my ($content, $status) = $inflate->inflate(\$buffer);
565             warn "DECOMPRESSED CONTENT: $content" if $DEBUG && $content;
566             if($status != +Z_OK or $status != +Z_STREAM_END)
567             {
568             warn 'INFLATE FAILED TO DECOMPRESS' if $DEBUG;
569             return POE::Filter::SimpleHTTP::Error->new
570             (
571             {
572             error => +INFLATE_FAILED_INFLATE,
573             context => $status
574             }
575             );
576             }
577             }
578            
579             } elsif($te =~ /compress/) {
580              
581             $content = Compress::Zlib::uncompress(\$buffer);
582             if(!defined($content))
583             {
584             warn 'UNCOMPRESS FAILED' if $DEBUG;
585             return POE::Filter::SimpleHTTP::Error->new
586             (
587             {
588             error => +UNCOMPRESS_FAILED
589             }
590             );
591             }
592              
593             } elsif($te =~ /gzip/) {
594              
595             warn 'BUFFER LENGTH BEFORE GUNZIP: '. length($buffer) if $DEBUG;
596             $content = Compress::Zlib::memGunzip(\$buffer);
597             warn "DECOMPRESSED CONTENT: $content" if $DEBUG;
598             if(!defined($content))
599             {
600             warn 'GUNZIP FAILED' if $DEBUG;
601             return POE::Filter::SimpleHTTP::Error->new
602             (
603             {
604             error => +GUNZIP_FAILED
605             }
606             );
607             }
608            
609             } else {
610            
611             warn 'UNKNOWN TRANSFER ENCOODING' if $DEBUG;
612             return POE::Filter::SimpleHTTP::Error->new
613             (
614             {
615             error => +UNKNOWN_TRANSFER_ENCODING,
616             context => $te
617             }
618             );
619             }
620             }
621              
622             $message->content_ref(\$content);
623            
624             } else {
625              
626             $message->add_content($_) for @{$self->content()};
627             }
628              
629             # We have the type, the headers, and the content. Return the object
630             return $message;
631             }
632              
633             =pod
634              
635             =head1 NAME
636              
637             POE::Filter::SimpleHTTP - A simple client/server suitable HTTP filter
638              
639             =head1 VERSION
640              
641             version 0.091710
642              
643             =head1 SYNOPSIS
644              
645             use POE::Filter::SimpleHTTP;
646             use HTTP::Request;
647             use HTTP::Respose;
648             use HTTP::Status;
649              
650             my $filter = POE::Filter::SimpleHTTP->new
651             (
652             {
653             mode => +PFSH_CLIENT,
654             useragent => 'Whizbang Client/0.01',
655             host => 'remote.server.com',
656             method => 'POST'
657             }
658             );
659              
660             my $post = $filter->put([qw|id=123& data=Here is some data|])->[0];
661              
662             =head1 DESCRIPTION
663              
664             POE::Filter::SimpleHTTP is a filter designed to be used in either a client or
665             a server context with the ability to switch the mode at runtime. In fact, a lot
666             of the behaviors can be altered at runtime. Which means you can put() just your
667             data into the filter and out the other side will be appropriate HTTP::Messages.
668              
669             =head1 PUBLIC ACCESSORS
670              
671             =over 4
672              
673             =item mode
674              
675             Use this access to change how the filter operates for put() if raw data is
676             passed in. In +PFSH_CLIENT mode, an HTTP::Request will be constructed using
677             data stored in other attributes of the filter. The obverse, if +PFSH_SERVER is
678             set, then HTTP::Responses will be built. Regardless of mode, all HTTP::Messages
679             passed to put() will be passed through without any modification. It defaults to
680             +PFSH_CLIENT.
681              
682             =item uri
683              
684             This accessor is used to change the URI part of the HTTP::Request objects built
685             in put() if raw data is passed. It can either be full on HTTP URI or an
686             absolute path. It defaults to '/'
687              
688             =item useragent
689              
690             Use this to change the user agent header on constructed HTTP::Request objects.
691             It defaults to __PACKAGE__ . '/' . $VERSION.
692              
693             =item host
694              
695             Use this to change the host header on constructed HTTP::Requests. It defaults
696             to 'localhost'
697              
698             =item status
699              
700             Use this to set the status codes for constructed HTTP::Responses. It defaults
701             to 200 (aka, HTTP_OK).
702              
703             =item method
704              
705             This accessor is used to change the method on constructed HTTP::Requests. It
706             defaults to 'GET'.
707              
708             =item mimetype
709              
710             This accessor is for the Content-Type header on constructed HTTP::Messages.
711             Regardless of mode(), constructed Requests and Responses will use this value.
712             It defaults to 'text/plain'
713              
714             =back
715              
716             =head1 PUBLIC METHODS
717              
718             This filter is based on POE::Filter and so only the differences in public API
719             will be mentioned below
720              
721             =over 4
722              
723             =item new()
724              
725             The constructor can be called with no arguments in which all of the defaults
726             mentioned above in the accessors will be used, or a hash or hashref may be
727             passed in with the keys corresponding to the accessors. Returns a new filter
728             instance.
729              
730             =item reset()
731              
732             This method will clear all of the internal buffers of the filter (but leave the
733             values provided to the accessors or constructor alone) back to their default
734             state.
735              
736             =item put()
737              
738             put() can accept either HTTP::Message based objects or raw data. If a Message
739             based object (ie. blessed($obj) && $obj->isa('HTTP::Message')) is passed in,
740             it will be passed out exactly as is, untouched.
741              
742             But if raw data is passed in, depending on mode(), it will construct a suitable
743             HTTP::Message (Request or Response) using the various values stored in the
744             above accessors, and return it.
745              
746             =back
747              
748             =head1 NOTES
749              
750             This is a simple filter in name and in implementation. Regardless of mode() the
751             get_one_start()/get_one() interface can accept both Responses and Requests. If
752             for whatever reason there is an error in parsing the data an Error object will
753             be returned with an particular constant, and a snippet of context (if available
754             at the time the error occurred). See POE::Filter::SimpleHTTP::Error for details
755             on what the objects look like.
756              
757             This filter should confrom to HTTP/0.9-HTTP/1.1 with regards to transfer
758             encodings (chunked, compressed, etc), in which case the data will be unchunked
759             and uncompressed and stored in the content() of the Message. Note that this
760             does not include Content-Encoding which HTTP::Message should handle for you.
761              
762             =head1 AUTHOR
763              
764             Copyright 2007 - 2009 Nicholas Perez.
765             Licensed and distributed under the GPL.
766              
767             =cut
768              
769             __PACKAGE__->meta->make_immutable();
770             no Moose;
771              
772             1;