File Coverage

blib/lib/Mojo/Content.pm
Criterion Covered Total %
statement 159 159 100.0
branch 104 106 98.1
condition 74 78 94.8
subroutine 39 39 100.0
pod 25 25 100.0
total 401 407 98.5


line stmt bran cond sub pod time code
1             package Mojo::Content;
2 64     64   367 use Mojo::Base 'Mojo::EventEmitter';
  64         111  
  64         433  
3              
4 64     64   430 use Carp qw(croak);
  64         123  
  64         3453  
5 64     64   852 use Compress::Raw::Zlib qw(WANT_GZIP Z_STREAM_END);
  64         5507  
  64         3251  
6 64     64   35844 use Mojo::Headers;
  64         173  
  64         387  
7 64     64   24931 use Mojo::SSE qw(build_event parse_event);
  64         171  
  64         4070  
8 64     64   364 use Scalar::Util qw(looks_like_number);
  64         112  
  64         171072  
9              
10             has [qw(auto_decompress auto_relax relaxed skip_body)];
11             has headers => sub { Mojo::Headers->new };
12             has max_buffer_size => sub { $ENV{MOJO_MAX_BUFFER_SIZE} || 262144 };
13             has max_leftover_size => sub { $ENV{MOJO_MAX_LEFTOVER_SIZE} || 262144 };
14              
15             my $BOUNDARY_RE = qr!multipart.*boundary\s*=\s*(?:"([^"]+)"|([\w'(),.:?\-+/]+))!i;
16              
17 1     1 1 2846 sub body_contains { croak 'Method "body_contains" not implemented by subclass' }
18 1     1 1 728 sub body_size { croak 'Method "body_size" not implemented by subclass' }
19              
20 3096 100 100 3096 1 5787 sub boundary { (shift->headers->content_type // '') =~ $BOUNDARY_RE ? $1 // $2 : undef }
      66        
21              
22             sub charset {
23 1080   100 1080 1 2966 my $type = shift->headers->content_type // '';
24 1080 100       10091 return $type =~ /charset\s*=\s*"?([^"\s;]+)"?/i ? $1 : undef;
25             }
26              
27             sub clone {
28 19     19 1 28 my $self = shift;
29 19 100       46 return undef if $self->is_dynamic;
30 15         41 return $self->new(headers => $self->headers->clone);
31             }
32              
33             sub generate_body_chunk {
34 245     245 1 407 my ($self, $offset) = @_;
35              
36 245 100 100     1171 $self->emit(drain => $offset) unless length($self->{body_buffer} //= '');
37 244 100       782 return delete $self->{body_buffer} if length $self->{body_buffer};
38 92 100       299 return '' if $self->{eof};
39              
40 24         94 my $len = $self->headers->content_length;
41 24 100 100     175 return looks_like_number $len && $len == $offset ? '' : undef;
42             }
43              
44 1     1 1 635 sub get_body_chunk { croak 'Method "get_body_chunk" not implemented by subclass' }
45              
46 2174     2174 1 3715 sub get_header_chunk { substr shift->_headers->{header_buffer}, shift, 131072 }
47              
48 2262     2262 1 5418 sub header_size { length shift->_headers->{header_buffer} }
49              
50 62     62 1 141 sub headers_contain { index(shift->_headers->{header_buffer}, shift) >= 0 }
51              
52 7444     7444 1 11664 sub is_chunked { !!shift->headers->transfer_encoding }
53              
54 1002   100 1002 1 1974 sub is_compressed { lc(shift->headers->content_encoding // '') eq 'gzip' }
55              
56 7473     7473 1 16781 sub is_dynamic { !!$_[0]{dynamic} }
57              
58 5395   100 5395 1 16596 sub is_finished { (shift->{state} // '') eq 'finished' }
59              
60 2838     2838 1 5789 sub is_limit_exceeded { !!shift->{limit} }
61              
62 4549     4549 1 13686 sub is_multipart {undef}
63              
64 21   100 21 1 108 sub is_parsing_body { (shift->{state} // '') eq 'body' }
65              
66 2583   100 2583 1 4242 sub is_sse { (shift->headers->content_type // '') eq 'text/event-stream' }
67              
68 1056     1056 1 3471 sub leftovers { shift->{buffer} }
69              
70             sub parse {
71 3035     3035 1 3758 my $self = shift;
72              
73             # Headers
74 3035         6347 $self->_parse_until_body(@_);
75 3035 100       5822 return $self if $self->{state} eq 'headers';
76              
77             # Chunked content
78 2724   100     8475 $self->{real_size} //= 0;
79 2724 100       5379 if ($self->is_chunked) {
    100          
80 141         377 $self->_parse_chunked;
81 141 100 100     467 $self->{state} = 'finished' if ($self->{chunk_state} // '') eq 'finished';
82             }
83              
84             # SSE
85 22         64 elsif ($self->is_sse) { $self->_parse_sse }
86              
87             # Not chunked, pass through to second buffer
88             else {
89 2561         4052 $self->{real_size} += length $self->{pre_buffer};
90 2561   100     4509 my $limit = $self->is_finished && length($self->{buffer}) > $self->max_leftover_size;
91 2561 100       8774 $self->{buffer} .= $self->{pre_buffer} unless $limit;
92 2561         4275 $self->{pre_buffer} = '';
93             }
94              
95             # No content
96 2724 100       6008 if ($self->skip_body) {
97 123         208 $self->{state} = 'finished';
98 123         463 return $self;
99             }
100              
101             # Relaxed parsing
102 2601         4402 my $headers = $self->headers;
103 2601   100     5011 my $len = $headers->content_length // '';
104 2601 100 100     5080 if ($self->auto_relax && !length $len) {
105 114   100     263 my $connection = lc($headers->connection // '');
106 114 100 100     447 $self->relaxed(1) if $connection eq 'close' || !$connection;
107             }
108              
109             # Chunked or relaxed content
110 2601 100 100     4076 if ($self->is_chunked || $self->relaxed) {
111 289   100     984 $self->_decompress($self->{buffer} //= '');
112 289         610 $self->{size} += length $self->{buffer};
113 289         444 $self->{buffer} = '';
114 289         982 return $self;
115             }
116              
117             # Normal content
118 2312 100       8580 $len = 0 unless looks_like_number $len;
119 2312 100 100     10089 if ((my $need = $len - ($self->{size} ||= 0)) > 0) {
120 1241         1957 my $len = length $self->{buffer};
121 1241 100       5886 my $chunk = substr $self->{buffer}, 0, $need > $len ? $len : $need, '';
122 1241         3444 $self->_decompress($chunk);
123 1241         2403 $self->{size} += length $chunk;
124             }
125 2312 100       4817 $self->{state} = 'finished' if $len <= $self->progress;
126              
127 2312         7738 return $self;
128             }
129              
130             sub parse_body {
131 57     57 1 68 my $self = shift;
132 57         122 $self->{state} = 'body';
133 57         157 return $self->parse(@_);
134             }
135              
136             sub progress {
137 2354     2354 1 2955 my $self = shift;
138 2354 100       4663 return 0 unless my $state = $self->{state};
139 2347 100 100     4805 return 0 unless $state eq 'body' || $state eq 'finished';
140 2341   100     7351 return $self->{raw_size} - ($self->{header_size} || 0);
141             }
142              
143             sub write {
144 152     152 1 293 my ($self, $chunk, $cb) = @_;
145              
146 152         220 $self->{dynamic} = 1;
147 152 100       389 $self->{body_buffer} .= $chunk if defined $chunk;
148 152 100       304 $self->once(drain => $cb) if $cb;
149 152 100 100     545 $self->{eof} = 1 if defined $chunk && !length $chunk;
150              
151 152         315 return $self;
152             }
153              
154             sub write_chunk {
155 110     110 1 497 my ($self, $chunk, $cb) = @_;
156              
157 110 100       286 $self->headers->transfer_encoding('chunked') unless $self->{chunked};
158 110         197 @{$self}{qw(chunked dynamic)} = (1, 1);
  110         213  
159              
160 110 100       432 $self->{body_buffer} .= $self->_build_chunk($chunk) if defined $chunk;
161 110 100       415 $self->once(drain => $cb) if $cb;
162 110 100 100     415 $self->{eof} = 1 if defined $chunk && !length $chunk;
163              
164 110         217 return $self;
165             }
166              
167             sub write_sse {
168 57     57 1 87 my ($self, $event, $cb) = @_;
169              
170 57 100       157 $self->headers->content_type('text/event-stream') unless $self->{sse};
171 57         85 $self->{sse} = 1;
172              
173 57 100       108 return $self->write unless defined $event;
174 55         235 return $self->write(build_event($event), $cb);
175             }
176              
177             sub _build_chunk {
178 109     109   181 my ($self, $chunk) = @_;
179              
180             # End
181 109 100       247 return "\x0d\x0a0\x0d\x0a\x0d\x0a" unless length $chunk;
182              
183             # First chunk has no leading CRLF
184 87 100       229 my $crlf = $self->{chunks}++ ? "\x0d\x0a" : '';
185 87         458 return $crlf . sprintf('%x', length $chunk) . "\x0d\x0a$chunk";
186             }
187              
188             sub _decompress {
189 1530     1530   2575 my ($self, $chunk) = @_;
190              
191             # No compression
192 1530 100 100     2919 return $self->emit(read => $chunk) unless $self->auto_decompress && $self->is_compressed;
193              
194             # Decompress
195 62         235 $self->{post_buffer} .= $chunk;
196 62   66     680 my $gz = $self->{gz} //= Compress::Raw::Zlib::Inflate->new(WindowBits => WANT_GZIP);
197 62         32270 my $status = $gz->inflate(\$self->{post_buffer}, my $out);
198 62 50       412 $self->emit(read => $out) if defined $out;
199              
200             # Replace Content-Encoding with Content-Length
201 62 100       265 $self->headers->content_length($gz->total_out)->remove('Content-Encoding') if $status == Z_STREAM_END;
202              
203             # Check buffer size
204 62 100 50     362 @$self{qw(state limit)} = ('finished', 1) if length($self->{post_buffer} // '') > $self->max_buffer_size;
205             }
206              
207             sub _headers {
208 4498     4498   4859 my $self = shift;
209 4498 100       11894 return $self if defined $self->{header_buffer};
210 2124         3908 my $headers = $self->headers->to_string;
211 2124 100       6543 $self->{header_buffer} = $headers ? "$headers\x0d\x0a\x0d\x0a" : "\x0d\x0a";
212 2124         8515 return $self;
213             }
214              
215             sub _parse_chunked {
216 141     141   174 my $self = shift;
217              
218             # Trailing headers
219 141 100 100     491 return $self->_parse_chunked_trailing_headers if ($self->{chunk_state} // '') eq 'trailing_headers';
220              
221 139         317 while (my $len = length $self->{pre_buffer}) {
222              
223             # Start new chunk (ignore the chunk extension)
224 261 100       449 unless ($self->{chunk_len}) {
225 156 100       821 last unless $self->{pre_buffer} =~ s/^(?:\x0d?\x0a)?([0-9a-fA-F]+).*\x0a//;
226 136 100       560 next if $self->{chunk_len} = hex $1;
227              
228             # Last chunk
229 31         76 $self->{chunk_state} = 'trailing_headers';
230 31         78 last;
231             }
232              
233             # Remove as much as possible from payload
234 105 100       261 $len = $self->{chunk_len} if $self->{chunk_len} < $len;
235 105         285 $self->{buffer} .= substr $self->{pre_buffer}, 0, $len, '';
236 105         177 $self->{real_size} += $len;
237 105         214 $self->{chunk_len} -= $len;
238             }
239              
240             # Trailing headers
241 139 100 100     560 $self->_parse_chunked_trailing_headers if ($self->{chunk_state} // '') eq 'trailing_headers';
242              
243             # Check buffer size
244 139 100 100     474 @$self{qw(state limit)} = ('finished', 1) if length($self->{pre_buffer} // '') > $self->max_buffer_size;
245             }
246              
247             sub _parse_chunked_trailing_headers {
248 33     33   71 my $self = shift;
249              
250 33         85 my $headers = $self->headers->parse(delete $self->{pre_buffer});
251 33 100       101 return unless $headers->is_finished;
252 31         60 $self->{chunk_state} = 'finished';
253              
254             # Take care of leftover and replace Transfer-Encoding with Content-Length
255 31         96 $self->{buffer} .= $headers->leftovers;
256 31         110 $headers->remove('Transfer-Encoding');
257 31 100       73 $headers->content_length($self->{real_size}) unless $headers->content_length;
258             }
259              
260             sub _parse_headers {
261 2882     2882   3341 my $self = shift;
262              
263 2882         5507 my $headers = $self->headers->parse(delete $self->{pre_buffer});
264 2882 100       7132 return unless $headers->is_finished;
265 2260         4056 $self->{state} = 'body';
266              
267             # Take care of leftovers
268 2260         4574 my $leftovers = $self->{pre_buffer} = $headers->leftovers;
269 2260         5971 $self->{header_size} = $self->{raw_size} - length $leftovers;
270             }
271              
272             sub _parse_sse {
273 22     22   32 my $self = shift;
274              
275             # Connection established
276 22 100       69 $self->emit('sse') unless $self->{sse};
277 22         54 $self->{sse} = 1;
278              
279             # Parse SSE
280 22         82 while (my $event = parse_event(\$self->{pre_buffer})) { $self->emit(sse => $event) }
  53         103  
281              
282             # Check buffer size
283 22 50 50     95 @$self{qw(state limit)} = ('finished', 1) if length($self->{pre_buffer} // '') > $self->max_buffer_size;
284             }
285              
286             sub _parse_until_body {
287 5854     5854   10022 my ($self, $chunk) = @_;
288              
289 5854   100     14852 $self->{raw_size} += length($chunk //= '');
290 5854         12844 $self->{pre_buffer} .= $chunk;
291 5854 100 100     19443 $self->_parse_headers if ($self->{state} ||= 'headers') eq 'headers';
292 5854 100 100     21687 $self->emit('body') if $self->{state} ne 'headers' && !$self->{body}++;
293             }
294              
295             1;
296              
297             =encoding utf8
298              
299             =head1 NAME
300              
301             Mojo::Content - HTTP content base class
302              
303             =head1 SYNOPSIS
304              
305             package Mojo::Content::MyContent;
306             use Mojo::Base 'Mojo::Content';
307              
308             sub body_contains {...}
309             sub body_size {...}
310             sub get_body_chunk {...}
311              
312             =head1 DESCRIPTION
313              
314             L is an abstract base class for HTTP content containers, based on L
315             7230|https://tools.ietf.org/html/rfc7230> and L, like
316             L and L.
317              
318             =head1 EVENTS
319              
320             L inherits all events from L and can emit the following new ones.
321              
322             =head2 body
323              
324             $content->on(body => sub ($content) {...});
325              
326             Emitted once all headers have been parsed and the body starts.
327              
328             $content->on(body => sub ($content) {
329             $content->auto_upgrade(0) if $content->headers->header('X-No-MultiPart');
330             });
331              
332             =head2 drain
333              
334             $content->on(drain => sub ($content, $offset) {...});
335              
336             Emitted once all data has been written.
337              
338             $content->on(drain => sub ($content) {
339             $content->write_chunk(time);
340             });
341              
342             =head2 read
343              
344             $content->on(read => sub ($content, $bytes) {...});
345              
346             Emitted when a new chunk of content arrives.
347              
348             $content->on(read => sub ($content, $bytes) {
349             say "Streaming: $bytes";
350             });
351              
352             =head2 sse
353              
354             $content->on(sse => sub ($content, $event) {...});
355              
356             Emitted when a new Server-Sent Event (SSE) connection has been established and for each new event that arrives. Note
357             that this event is B and may change without warning!
358              
359             $content->on(sse => sub ($content, $event) {
360             if ($event) { say "Type: $event->{type}, Data: $event->{data}" }
361             else { say "SSE connection established" }
362             });
363              
364             =head1 ATTRIBUTES
365              
366             L implements the following attributes.
367              
368             =head2 auto_decompress
369              
370             my $bool = $content->auto_decompress;
371             $content = $content->auto_decompress($bool);
372              
373             Decompress content automatically if L is true.
374              
375             =head2 auto_relax
376              
377             my $bool = $content->auto_relax;
378             $content = $content->auto_relax($bool);
379              
380             Try to detect when relaxed parsing is necessary.
381              
382             =head2 headers
383              
384             my $headers = $content->headers;
385             $content = $content->headers(Mojo::Headers->new);
386              
387             Content headers, defaults to a L object.
388              
389             =head2 max_buffer_size
390              
391             my $size = $content->max_buffer_size;
392             $content = $content->max_buffer_size(1024);
393              
394             Maximum size in bytes of buffer for content parser, defaults to the value of the C environment
395             variable or C<262144> (256KiB).
396              
397             =head2 max_leftover_size
398              
399             my $size = $content->max_leftover_size;
400             $content = $content->max_leftover_size(1024);
401              
402             Maximum size in bytes of buffer for pipelined HTTP requests, defaults to the value of the C
403             environment variable or C<262144> (256KiB).
404              
405             =head2 relaxed
406              
407             my $bool = $content->relaxed;
408             $content = $content->relaxed($bool);
409              
410             Activate relaxed parsing for responses that are terminated with a connection close.
411              
412             =head2 skip_body
413              
414             my $bool = $content->skip_body;
415             $content = $content->skip_body($bool);
416              
417             Skip body parsing and finish after headers.
418              
419             =head1 METHODS
420              
421             L inherits all methods from L and implements the following new ones.
422              
423             =head2 body_contains
424              
425             my $bool = $content->body_contains('foo bar baz');
426              
427             Check if content contains a specific string. Meant to be overloaded in a subclass.
428              
429             =head2 body_size
430              
431             my $size = $content->body_size;
432              
433             Content size in bytes. Meant to be overloaded in a subclass.
434              
435             =head2 boundary
436              
437             my $boundary = $content->boundary;
438              
439             Extract multipart boundary from C header.
440              
441             =head2 charset
442              
443             my $charset = $content->charset;
444              
445             Extract charset from C header.
446              
447             =head2 clone
448              
449             my $clone = $content->clone;
450              
451             Return a new L object cloned from this content if possible, otherwise return C.
452              
453             =head2 generate_body_chunk
454              
455             my $bytes = $content->generate_body_chunk(0);
456              
457             Generate dynamic content.
458              
459             =head2 get_body_chunk
460              
461             my $bytes = $content->get_body_chunk(0);
462              
463             Get a chunk of content starting from a specific position. Meant to be overloaded in a subclass.
464              
465             =head2 get_header_chunk
466              
467             my $bytes = $content->get_header_chunk(13);
468              
469             Get a chunk of the headers starting from a specific position. Note that this method finalizes the content.
470              
471             =head2 header_size
472              
473             my $size = $content->header_size;
474              
475             Size of headers in bytes. Note that this method finalizes the content.
476              
477             =head2 headers_contain
478              
479             my $bool = $content->headers_contain('foo bar baz');
480              
481             Check if headers contain a specific string. Note that this method finalizes the content.
482              
483             =head2 is_chunked
484              
485             my $bool = $content->is_chunked;
486              
487             Check if C header indicates chunked transfer encoding.
488              
489             =head2 is_compressed
490              
491             my $bool = $content->is_compressed;
492              
493             Check C header for C value.
494              
495             =head2 is_dynamic
496              
497             my $bool = $content->is_dynamic;
498              
499             Check if content will be dynamically generated, which prevents L from working.
500              
501             =head2 is_finished
502              
503             my $bool = $content->is_finished;
504              
505             Check if parser is finished.
506              
507             =head2 is_limit_exceeded
508              
509             my $bool = $content->is_limit_exceeded;
510              
511             Check if buffer has exceeded L.
512              
513             =head2 is_multipart
514              
515             my $bool = $content->is_multipart;
516              
517             False, this is not a L object.
518              
519             =head2 is_parsing_body
520              
521             my $bool = $content->is_parsing_body;
522              
523             Check if body parsing started yet.
524              
525             =head2 is_sse
526              
527             my $bool = $content->is_sse;
528              
529             Check if C header indicates Server-Sent Events (SSE). Note that this method is B and may
530             change without warning!
531              
532             =head2 leftovers
533              
534             my $bytes = $content->leftovers;
535              
536             Get leftover data from content parser.
537              
538             =head2 parse
539              
540             $content
541             = $content->parse("Content-Length: 12\x0d\x0a\x0d\x0aHello World!");
542              
543             Parse content chunk.
544              
545             =head2 parse_body
546              
547             $content = $content->parse_body('Hi!');
548              
549             Parse body chunk and skip headers.
550              
551             =head2 progress
552              
553             my $size = $content->progress;
554              
555             Size of content already received from message in bytes.
556              
557             =head2 write
558              
559             $content = $content->write;
560             $content = $content->write('');
561             $content = $content->write($bytes);
562             $content = $content->write($bytes => sub {...});
563              
564             Write dynamic content non-blocking, the optional drain callback will be executed once all data has been written.
565             Calling this method without a chunk of data will finalize the L and allow for dynamic content to be written
566             later. You can write an empty chunk of data at any time to end the stream.
567              
568             # Make sure previous chunk of data has been written before continuing
569             $content->write('He' => sub ($content) {
570             $content->write('llo!' => sub ($content) {
571             $content->write('');
572             });
573             });
574              
575             =head2 write_chunk
576              
577             $content = $content->write_chunk;
578             $content = $content->write_chunk('');
579             $content = $content->write_chunk($bytes);
580             $content = $content->write_chunk($bytes => sub {...});
581              
582             Write dynamic content non-blocking with chunked transfer encoding, the optional drain callback will be executed once
583             all data has been written. Calling this method without a chunk of data will finalize the L and allow for
584             dynamic content to be written later. You can write an empty chunk of data at any time to end the stream.
585              
586             # Make sure previous chunk of data has been written before continuing
587             $content->write_chunk('He' => sub ($content) {
588             $content->write_chunk('llo!' => sub ($content) {
589             $content->write_chunk('');
590             });
591             });
592              
593             =head2 write_sse
594              
595             $content = $content->write_sse;
596             $content = $content->write_sse($event);
597             $content = $content->write_sse($event => sub {...});
598              
599             Write Server-Sent Event (SSE) non-blocking, the optional drain callback will be executed once all data has been
600             written. Calling this method without an event will finalize the response headers and allow for events to be written
601             later. Note that this method is B and may change without warning!
602              
603             =head1 SEE ALSO
604              
605             L, L, L.
606              
607             =cut