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   471 use Mojo::Base 'Mojo::EventEmitter';
  64         137  
  64         544  
3              
4 64     64   467 use Carp qw(croak);
  64         133  
  64         4339  
5 64     64   1253 use Compress::Raw::Zlib qw(WANT_GZIP Z_STREAM_END);
  64         7888  
  64         4334  
6 64     64   37499 use Mojo::Headers;
  64         238  
  64         509  
7 64     64   33988 use Mojo::SSE qw(build_event parse_event);
  64         209  
  64         5448  
8 64     64   549 use Scalar::Util qw(looks_like_number);
  64         150  
  64         270126  
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 2773 sub body_contains { croak 'Method "body_contains" not implemented by subclass' }
18 1     1 1 715 sub body_size { croak 'Method "body_size" not implemented by subclass' }
19              
20 3066 100 100 3066 1 9028 sub boundary { (shift->headers->content_type // '') =~ $BOUNDARY_RE ? $1 // $2 : undef }
      66        
21              
22             sub charset {
23 1080   100 1080 1 4367 my $type = shift->headers->content_type // '';
24 1080 100       12914 return $type =~ /charset\s*=\s*"?([^"\s;]+)"?/i ? $1 : undef;
25             }
26              
27             sub clone {
28 19     19 1 48 my $self = shift;
29 19 100       98 return undef if $self->is_dynamic;
30 15         66 return $self->new(headers => $self->headers->clone);
31             }
32              
33             sub generate_body_chunk {
34 245     245 1 466 my ($self, $offset) = @_;
35              
36 245 100 100     1679 $self->emit(drain => $offset) unless length($self->{body_buffer} //= '');
37 244 100       1375 return delete $self->{body_buffer} if length $self->{body_buffer};
38 92 100       477 return '' if $self->{eof};
39              
40 25         145 my $len = $self->headers->content_length;
41 25 100 100     244 return looks_like_number $len && $len == $offset ? '' : undef;
42             }
43              
44 1     1 1 660 sub get_body_chunk { croak 'Method "get_body_chunk" not implemented by subclass' }
45              
46 2174     2174 1 5759 sub get_header_chunk { substr shift->_headers->{header_buffer}, shift, 131072 }
47              
48 2262     2262 1 8082 sub header_size { length shift->_headers->{header_buffer} }
49              
50 62     62 1 158 sub headers_contain { index(shift->_headers->{header_buffer}, shift) >= 0 }
51              
52 7406     7406 1 16962 sub is_chunked { !!shift->headers->transfer_encoding }
53              
54 1000   100 1000 1 2850 sub is_compressed { lc(shift->headers->content_encoding // '') eq 'gzip' }
55              
56 7473     7473 1 24245 sub is_dynamic { !!$_[0]{dynamic} }
57              
58 5359   100 5359 1 24710 sub is_finished { (shift->{state} // '') eq 'finished' }
59              
60 2819     2819 1 8614 sub is_limit_exceeded { !!shift->{limit} }
61              
62 4549     4549 1 19271 sub is_multipart {undef}
63              
64 21   100 21 1 130 sub is_parsing_body { (shift->{state} // '') eq 'body' }
65              
66 2566   100 2566 1 6446 sub is_sse { (shift->headers->content_type // '') eq 'text/event-stream' }
67              
68 1056     1056 1 5064 sub leftovers { shift->{buffer} }
69              
70             sub parse {
71 3005     3005 1 6091 my $self = shift;
72              
73             # Headers
74 3005         9383 $self->_parse_until_body(@_);
75 3005 100       8639 return $self if $self->{state} eq 'headers';
76              
77             # Chunked content
78 2705   100     12704 $self->{real_size} //= 0;
79 2705 100       7787 if ($self->is_chunked) {
    100          
80 139         570 $self->_parse_chunked;
81 139 100 100     718 $self->{state} = 'finished' if ($self->{chunk_state} // '') eq 'finished';
82             }
83              
84             # SSE
85 22         83 elsif ($self->is_sse) { $self->_parse_sse }
86              
87             # Not chunked, pass through to second buffer
88             else {
89 2544         6121 $self->{real_size} += length $self->{pre_buffer};
90 2544   100     6476 my $limit = $self->is_finished && length($self->{buffer}) > $self->max_leftover_size;
91 2544 100       14098 $self->{buffer} .= $self->{pre_buffer} unless $limit;
92 2544         5975 $self->{pre_buffer} = '';
93             }
94              
95             # No content
96 2705 100       9271 if ($self->skip_body) {
97 123         281 $self->{state} = 'finished';
98 123         617 return $self;
99             }
100              
101             # Relaxed parsing
102 2582         6310 my $headers = $self->headers;
103 2582   100     6765 my $len = $headers->content_length // '';
104 2582 100 100     8196 if ($self->auto_relax && !length $len) {
105 112   100     350 my $connection = lc($headers->connection // '');
106 112 100 100     636 $self->relaxed(1) if $connection eq 'close' || !$connection;
107             }
108              
109             # Chunked or relaxed content
110 2582 100 100     6062 if ($self->is_chunked || $self->relaxed) {
111 287   100     1380 $self->_decompress($self->{buffer} //= '');
112 287         863 $self->{size} += length $self->{buffer};
113 287         599 $self->{buffer} = '';
114 287         1457 return $self;
115             }
116              
117             # Normal content
118 2295 100       12700 $len = 0 unless looks_like_number $len;
119 2295 100 100     15282 if ((my $need = $len - ($self->{size} ||= 0)) > 0) {
120 1224         2912 my $len = length $self->{buffer};
121 1224 100       9487 my $chunk = substr $self->{buffer}, 0, $need > $len ? $len : $need, '';
122 1224         4789 $self->_decompress($chunk);
123 1224         3320 $self->{size} += length $chunk;
124             }
125 2295 100       7364 $self->{state} = 'finished' if $len <= $self->progress;
126              
127 2295         11938 return $self;
128             }
129              
130             sub parse_body {
131 57     57 1 95 my $self = shift;
132 57         195 $self->{state} = 'body';
133 57         216 return $self->parse(@_);
134             }
135              
136             sub progress {
137 2337     2337 1 4310 my $self = shift;
138 2337 100       7037 return 0 unless my $state = $self->{state};
139 2330 100 100     7475 return 0 unless $state eq 'body' || $state eq 'finished';
140 2324   100     10714 return $self->{raw_size} - ($self->{header_size} || 0);
141             }
142              
143             sub write {
144 152     152 1 470 my ($self, $chunk, $cb) = @_;
145              
146 152         348 $self->{dynamic} = 1;
147 152 100       509 $self->{body_buffer} .= $chunk if defined $chunk;
148 152 100       382 $self->once(drain => $cb) if $cb;
149 152 100 100     711 $self->{eof} = 1 if defined $chunk && !length $chunk;
150              
151 152         506 return $self;
152             }
153              
154             sub write_chunk {
155 110     110 1 493 my ($self, $chunk, $cb) = @_;
156              
157 110 100       389 $self->headers->transfer_encoding('chunked') unless $self->{chunked};
158 110         217 @{$self}{qw(chunked dynamic)} = (1, 1);
  110         320  
159              
160 110 100       586 $self->{body_buffer} .= $self->_build_chunk($chunk) if defined $chunk;
161 110 100       566 $self->once(drain => $cb) if $cb;
162 110 100 100     577 $self->{eof} = 1 if defined $chunk && !length $chunk;
163              
164 110         316 return $self;
165             }
166              
167             sub write_sse {
168 57     57 1 130 my ($self, $event, $cb) = @_;
169              
170 57 100       193 $self->headers->content_type('text/event-stream') unless $self->{sse};
171 57         113 $self->{sse} = 1;
172              
173 57 100       139 return $self->write unless defined $event;
174 55         197 return $self->write(build_event($event), $cb);
175             }
176              
177             sub _build_chunk {
178 109     109   233 my ($self, $chunk) = @_;
179              
180             # End
181 109 100       333 return "\x0d\x0a0\x0d\x0a\x0d\x0a" unless length $chunk;
182              
183             # First chunk has no leading CRLF
184 87 100       347 my $crlf = $self->{chunks}++ ? "\x0d\x0a" : '';
185 87         601 return $crlf . sprintf('%x', length $chunk) . "\x0d\x0a$chunk";
186             }
187              
188             sub _decompress {
189 1511     1511   3848 my ($self, $chunk) = @_;
190              
191             # No compression
192 1511 100 100     4365 return $self->emit(read => $chunk) unless $self->auto_decompress && $self->is_compressed;
193              
194             # Decompress
195 62         335 $self->{post_buffer} .= $chunk;
196 62   66     879 my $gz = $self->{gz} //= Compress::Raw::Zlib::Inflate->new(WindowBits => WANT_GZIP);
197 62         44118 my $status = $gz->inflate(\$self->{post_buffer}, my $out);
198 62 50       620 $self->emit(read => $out) if defined $out;
199              
200             # Replace Content-Encoding with Content-Length
201 62 100       399 $self->headers->content_length($gz->total_out)->remove('Content-Encoding') if $status == Z_STREAM_END;
202              
203             # Check buffer size
204 62 100 50     540 @$self{qw(state limit)} = ('finished', 1) if length($self->{post_buffer} // '') > $self->max_buffer_size;
205             }
206              
207             sub _headers {
208 4498     4498   7189 my $self = shift;
209 4498 100       18127 return $self if defined $self->{header_buffer};
210 2124         5692 my $headers = $self->headers->to_string;
211 2124 100       11607 $self->{header_buffer} = $headers ? "$headers\x0d\x0a\x0d\x0a" : "\x0d\x0a";
212 2124         13422 return $self;
213             }
214              
215             sub _parse_chunked {
216 139     139   240 my $self = shift;
217              
218             # Trailing headers
219 139 100 100     754 return $self->_parse_chunked_trailing_headers if ($self->{chunk_state} // '') eq 'trailing_headers';
220              
221 137         505 while (my $len = length $self->{pre_buffer}) {
222              
223             # Start new chunk (ignore the chunk extension)
224 261 100       636 unless ($self->{chunk_len}) {
225 156 100       1323 last unless $self->{pre_buffer} =~ s/^(?:\x0d?\x0a)?([0-9a-fA-F]+).*\x0a//;
226 136 100       812 next if $self->{chunk_len} = hex $1;
227              
228             # Last chunk
229 31         134 $self->{chunk_state} = 'trailing_headers';
230 31         89 last;
231             }
232              
233             # Remove as much as possible from payload
234 105 100       295 $len = $self->{chunk_len} if $self->{chunk_len} < $len;
235 105         446 $self->{buffer} .= substr $self->{pre_buffer}, 0, $len, '';
236 105         189 $self->{real_size} += $len;
237 105         312 $self->{chunk_len} -= $len;
238             }
239              
240             # Trailing headers
241 137 100 100     850 $self->_parse_chunked_trailing_headers if ($self->{chunk_state} // '') eq 'trailing_headers';
242              
243             # Check buffer size
244 137 100 100     813 @$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   84 my $self = shift;
249              
250 33         161 my $headers = $self->headers->parse(delete $self->{pre_buffer});
251 33 100       155 return unless $headers->is_finished;
252 31         96 $self->{chunk_state} = 'finished';
253              
254             # Take care of leftover and replace Transfer-Encoding with Content-Length
255 31         129 $self->{buffer} .= $headers->leftovers;
256 31         196 $headers->remove('Transfer-Encoding');
257 31 100       107 $headers->content_length($self->{real_size}) unless $headers->content_length;
258             }
259              
260             sub _parse_headers {
261 2860     2860   4782 my $self = shift;
262              
263 2860         7911 my $headers = $self->headers->parse(delete $self->{pre_buffer});
264 2860 100       10908 return unless $headers->is_finished;
265 2260         5740 $self->{state} = 'body';
266              
267             # Take care of leftovers
268 2260         7434 my $leftovers = $self->{pre_buffer} = $headers->leftovers;
269 2260         8744 $self->{header_size} = $self->{raw_size} - length $leftovers;
270             }
271              
272             sub _parse_sse {
273 22     22   48 my $self = shift;
274              
275             # Connection established
276 22 100       111 $self->emit('sse') unless $self->{sse};
277 22         60 $self->{sse} = 1;
278              
279             # Parse SSE
280 22         106 while (my $event = parse_event(\$self->{pre_buffer})) { $self->emit(sse => $event) }
  53         171  
281              
282             # Check buffer size
283 22 50 50     130 @$self{qw(state limit)} = ('finished', 1) if length($self->{pre_buffer} // '') > $self->max_buffer_size;
284             }
285              
286             sub _parse_until_body {
287 5811     5811   12846 my ($self, $chunk) = @_;
288              
289 5811   100     21271 $self->{raw_size} += length($chunk //= '');
290 5811         25704 $self->{pre_buffer} .= $chunk;
291 5811 100 100     29432 $self->_parse_headers if ($self->{state} ||= 'headers') eq 'headers';
292 5811 100 100     33830 $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