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   572 use Mojo::Base 'Mojo::EventEmitter';
  64         162  
  64         594  
3              
4 64     64   530 use Carp qw(croak);
  64         152  
  64         4862  
5 64     64   1070 use Compress::Raw::Zlib qw(WANT_GZIP Z_STREAM_END);
  64         5759  
  64         4690  
6 64     64   39150 use Mojo::Headers;
  64         249  
  64         556  
7 64     64   37379 use Mojo::SSE qw(build_event parse_event);
  64         287  
  64         5717  
8 64     64   600 use Scalar::Util qw(looks_like_number);
  64         139  
  64         272997  
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 3357 sub body_contains { croak 'Method "body_contains" not implemented by subclass' }
18 1     1 1 1190 sub body_size { croak 'Method "body_size" not implemented by subclass' }
19              
20 3050 100 100 3050 1 10170 sub boundary { (shift->headers->content_type // '') =~ $BOUNDARY_RE ? $1 // $2 : undef }
      66        
21              
22             sub charset {
23 1080   100 1080 1 4097 my $type = shift->headers->content_type // '';
24 1080 100       13704 return $type =~ /charset\s*=\s*"?([^"\s;]+)"?/i ? $1 : undef;
25             }
26              
27             sub clone {
28 19     19 1 49 my $self = shift;
29 19 100       81 return undef if $self->is_dynamic;
30 15         60 return $self->new(headers => $self->headers->clone);
31             }
32              
33             sub generate_body_chunk {
34 245     245 1 485 my ($self, $offset) = @_;
35              
36 245 100 100     1493 $self->emit(drain => $offset) unless length($self->{body_buffer} //= '');
37 244 100       1074 return delete $self->{body_buffer} if length $self->{body_buffer};
38 92 100       423 return '' if $self->{eof};
39              
40 25         150 my $len = $self->headers->content_length;
41 25 100 100     302 return looks_like_number $len && $len == $offset ? '' : undef;
42             }
43              
44 1     1 1 811 sub get_body_chunk { croak 'Method "get_body_chunk" not implemented by subclass' }
45              
46 2174     2174 1 7148 sub get_header_chunk { substr shift->_headers->{header_buffer}, shift, 131072 }
47              
48 2262     2262 1 9655 sub header_size { length shift->_headers->{header_buffer} }
49              
50 62     62 1 190 sub headers_contain { index(shift->_headers->{header_buffer}, shift) >= 0 }
51              
52 7384     7384 1 17732 sub is_chunked { !!shift->headers->transfer_encoding }
53              
54 999   100 999 1 3113 sub is_compressed { lc(shift->headers->content_encoding // '') eq 'gzip' }
55              
56 7473     7473 1 26855 sub is_dynamic { !!$_[0]{dynamic} }
57              
58 5337   100 5337 1 26285 sub is_finished { (shift->{state} // '') eq 'finished' }
59              
60 2809     2809 1 14627 sub is_limit_exceeded { !!shift->{limit} }
61              
62 4549     4549 1 20762 sub is_multipart {undef}
63              
64 21   100 21 1 147 sub is_parsing_body { (shift->{state} // '') eq 'body' }
65              
66 2554   100 2554 1 6733 sub is_sse { (shift->headers->content_type // '') eq 'text/event-stream' }
67              
68 1056     1056 1 5473 sub leftovers { shift->{buffer} }
69              
70             sub parse {
71 2989     2989 1 6350 my $self = shift;
72              
73             # Headers
74 2989         9388 $self->_parse_until_body(@_);
75 2989 100       10095 return $self if $self->{state} eq 'headers';
76              
77             # Chunked content
78 2694   100     13667 $self->{real_size} //= 0;
79 2694 100       7883 if ($self->is_chunked) {
    100          
80 140         496 $self->_parse_chunked;
81 140 100 100     568 $self->{state} = 'finished' if ($self->{chunk_state} // '') eq 'finished';
82             }
83              
84             # SSE
85 22         125 elsif ($self->is_sse) { $self->_parse_sse }
86              
87             # Not chunked, pass through to second buffer
88             else {
89 2532         6739 $self->{real_size} += length $self->{pre_buffer};
90 2532   100     7269 my $limit = $self->is_finished && length($self->{buffer}) > $self->max_leftover_size;
91 2532 100       13288 $self->{buffer} .= $self->{pre_buffer} unless $limit;
92 2532         6324 $self->{pre_buffer} = '';
93             }
94              
95             # No content
96 2694 100       10021 if ($self->skip_body) {
97 123         324 $self->{state} = 'finished';
98 123         611 return $self;
99             }
100              
101             # Relaxed parsing
102 2571         7293 my $headers = $self->headers;
103 2571   100     7988 my $len = $headers->content_length // '';
104 2571 100 100     8262 if ($self->auto_relax && !length $len) {
105 112   100     377 my $connection = lc($headers->connection // '');
106 112 100 100     681 $self->relaxed(1) if $connection eq 'close' || !$connection;
107             }
108              
109             # Chunked or relaxed content
110 2571 100 100     6774 if ($self->is_chunked || $self->relaxed) {
111 286   100     1460 $self->_decompress($self->{buffer} //= '');
112 286         860 $self->{size} += length $self->{buffer};
113 286         579 $self->{buffer} = '';
114 286         1530 return $self;
115             }
116              
117             # Normal content
118 2285 100       13728 $len = 0 unless looks_like_number $len;
119 2285 100 100     16319 if ((my $need = $len - ($self->{size} ||= 0)) > 0) {
120 1214         3055 my $len = length $self->{buffer};
121 1214 100       19269 my $chunk = substr $self->{buffer}, 0, $need > $len ? $len : $need, '';
122 1214         5494 $self->_decompress($chunk);
123 1214         3575 $self->{size} += length $chunk;
124             }
125 2285 100       7914 $self->{state} = 'finished' if $len <= $self->progress;
126              
127 2285         13102 return $self;
128             }
129              
130             sub parse_body {
131 57     57 1 103 my $self = shift;
132 57         151 $self->{state} = 'body';
133 57         247 return $self->parse(@_);
134             }
135              
136             sub progress {
137 2327     2327 1 4271 my $self = shift;
138 2327 100       8912 return 0 unless my $state = $self->{state};
139 2320 100 100     8858 return 0 unless $state eq 'body' || $state eq 'finished';
140 2314   100     11982 return $self->{raw_size} - ($self->{header_size} || 0);
141             }
142              
143             sub write {
144 152     152 1 430 my ($self, $chunk, $cb) = @_;
145              
146 152         329 $self->{dynamic} = 1;
147 152 100       615 $self->{body_buffer} .= $chunk if defined $chunk;
148 152 100       414 $self->once(drain => $cb) if $cb;
149 152 100 100     730 $self->{eof} = 1 if defined $chunk && !length $chunk;
150              
151 152         601 return $self;
152             }
153              
154             sub write_chunk {
155 110     110 1 524 my ($self, $chunk, $cb) = @_;
156              
157 110 100       445 $self->headers->transfer_encoding('chunked') unless $self->{chunked};
158 110         207 @{$self}{qw(chunked dynamic)} = (1, 1);
  110         373  
159              
160 110 100       607 $self->{body_buffer} .= $self->_build_chunk($chunk) if defined $chunk;
161 110 100       554 $self->once(drain => $cb) if $cb;
162 110 100 100     565 $self->{eof} = 1 if defined $chunk && !length $chunk;
163              
164 110         290 return $self;
165             }
166              
167             sub write_sse {
168 57     57 1 922 my ($self, $event, $cb) = @_;
169              
170 57 100       220 $self->headers->content_type('text/event-stream') unless $self->{sse};
171 57         126 $self->{sse} = 1;
172              
173 57 100       185 return $self->write unless defined $event;
174 55         213 return $self->write(build_event($event), $cb);
175             }
176              
177             sub _build_chunk {
178 109     109   261 my ($self, $chunk) = @_;
179              
180             # End
181 109 100       300 return "\x0d\x0a0\x0d\x0a\x0d\x0a" unless length $chunk;
182              
183             # First chunk has no leading CRLF
184 87 100       298 my $crlf = $self->{chunks}++ ? "\x0d\x0a" : '';
185 87         559 return $crlf . sprintf('%x', length $chunk) . "\x0d\x0a$chunk";
186             }
187              
188             sub _decompress {
189 1500     1500   4257 my ($self, $chunk) = @_;
190              
191             # No compression
192 1500 100 100     4496 return $self->emit(read => $chunk) unless $self->auto_decompress && $self->is_compressed;
193              
194             # Decompress
195 62         336 $self->{post_buffer} .= $chunk;
196 62   66     775 my $gz = $self->{gz} //= Compress::Raw::Zlib::Inflate->new(WindowBits => WANT_GZIP);
197 62         44921 my $status = $gz->inflate(\$self->{post_buffer}, my $out);
198 62 50       604 $self->emit(read => $out) if defined $out;
199              
200             # Replace Content-Encoding with Content-Length
201 62 100       363 $self->headers->content_length($gz->total_out)->remove('Content-Encoding') if $status == Z_STREAM_END;
202              
203             # Check buffer size
204 62 100 50     506 @$self{qw(state limit)} = ('finished', 1) if length($self->{post_buffer} // '') > $self->max_buffer_size;
205             }
206              
207             sub _headers {
208 4498     4498   7414 my $self = shift;
209 4498 100       22133 return $self if defined $self->{header_buffer};
210 2124         6608 my $headers = $self->headers->to_string;
211 2124 100       12038 $self->{header_buffer} = $headers ? "$headers\x0d\x0a\x0d\x0a" : "\x0d\x0a";
212 2124         14024 return $self;
213             }
214              
215             sub _parse_chunked {
216 140     140   210 my $self = shift;
217              
218             # Trailing headers
219 140 100 100     718 return $self->_parse_chunked_trailing_headers if ($self->{chunk_state} // '') eq 'trailing_headers';
220              
221 138         419 while (my $len = length $self->{pre_buffer}) {
222              
223             # Start new chunk (ignore the chunk extension)
224 261 100       555 unless ($self->{chunk_len}) {
225 156 100       1100 last unless $self->{pre_buffer} =~ s/^(?:\x0d?\x0a)?([0-9a-fA-F]+).*\x0a//;
226 136 100       683 next if $self->{chunk_len} = hex $1;
227              
228             # Last chunk
229 31         100 $self->{chunk_state} = 'trailing_headers';
230 31         90 last;
231             }
232              
233             # Remove as much as possible from payload
234 105 100       286 $len = $self->{chunk_len} if $self->{chunk_len} < $len;
235 105         445 $self->{buffer} .= substr $self->{pre_buffer}, 0, $len, '';
236 105         177 $self->{real_size} += $len;
237 105         285 $self->{chunk_len} -= $len;
238             }
239              
240             # Trailing headers
241 138 100 100     724 $self->_parse_chunked_trailing_headers if ($self->{chunk_state} // '') eq 'trailing_headers';
242              
243             # Check buffer size
244 138 100 100     594 @$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   74 my $self = shift;
249              
250 33         106 my $headers = $self->headers->parse(delete $self->{pre_buffer});
251 33 100       128 return unless $headers->is_finished;
252 31         87 $self->{chunk_state} = 'finished';
253              
254             # Take care of leftover and replace Transfer-Encoding with Content-Length
255 31         124 $self->{buffer} .= $headers->leftovers;
256 31         149 $headers->remove('Transfer-Encoding');
257 31 100       102 $headers->content_length($self->{real_size}) unless $headers->content_length;
258             }
259              
260             sub _parse_headers {
261 2850     2850   5133 my $self = shift;
262              
263 2850         8807 my $headers = $self->headers->parse(delete $self->{pre_buffer});
264 2850 100       12871 return unless $headers->is_finished;
265 2260         6447 $self->{state} = 'body';
266              
267             # Take care of leftovers
268 2260         8167 my $leftovers = $self->{pre_buffer} = $headers->leftovers;
269 2260         8976 $self->{header_size} = $self->{raw_size} - length $leftovers;
270             }
271              
272             sub _parse_sse {
273 22     22   53 my $self = shift;
274              
275             # Connection established
276 22 100       138 $self->emit('sse') unless $self->{sse};
277 22         91 $self->{sse} = 1;
278              
279             # Parse SSE
280 22         146 while (my $event = parse_event(\$self->{pre_buffer})) { $self->emit(sse => $event) }
  53         223  
281              
282             # Check buffer size
283 22 50 50     202 @$self{qw(state limit)} = ('finished', 1) if length($self->{pre_buffer} // '') > $self->max_buffer_size;
284             }
285              
286             sub _parse_until_body {
287 5788     5788   13303 my ($self, $chunk) = @_;
288              
289 5788   100     21780 $self->{raw_size} += length($chunk //= '');
290 5788         18575 $self->{pre_buffer} .= $chunk;
291 5788 100 100     31558 $self->_parse_headers if ($self->{state} ||= 'headers') eq 'headers';
292 5788 100 100     36396 $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