File Coverage

blib/lib/Protocol/HTTP2/HeaderCompression.pm
Criterion Covered Total %
statement 155 184 84.2
branch 40 66 60.6
condition 13 25 52.0
subroutine 15 15 100.0
pod 0 8 0.0
total 223 298 74.8


line stmt bran cond sub pod time code
1             package Protocol::HTTP2::HeaderCompression;
2 11     11   52 use strict;
  11         19  
  11         274  
3 11     11   67 use warnings;
  11         19  
  11         261  
4 11     11   5641 use Protocol::HTTP2::Huffman;
  11         27  
  11         603  
5 11     11   6125 use Protocol::HTTP2::StaticTable;
  11         26  
  11         1097  
6 11     11   62 use Protocol::HTTP2::Constants qw(:errors :settings :limits);
  11         19  
  11         3181  
7 11     11   56 use Protocol::HTTP2::Trace qw(tracer bin2hex);
  11         20  
  11         518  
8 11     11   50 use Exporter qw(import);
  11         18  
  11         24550  
9             our @EXPORT_OK = qw(int_encode int_decode str_encode str_decode headers_decode
10             headers_encode);
11              
12             sub int_encode {
13 236     236 0 40090 my ( $int, $N ) = @_;
14 236   50     38232 $N ||= 7;
15 236         38333 my $ff = ( 1 << $N ) - 1;
16              
17 236 100       38331 if ( $int < $ff ) {
18 233         76445 return pack 'C', $int;
19             }
20              
21 3         8 my $res = pack 'C', $ff;
22 3         5 $int -= $ff;
23              
24 3         10 while ( $int >= 0x80 ) {
25 1         4 $res .= pack( 'C', ( $int & 0x7f ) | 0x80 );
26 1         4 $int >>= 7;
27             }
28              
29 3         17 return $res . pack( 'C', $int );
30             }
31              
32             # int_decode()
33             #
34             # arguments:
35             # buf_ref - ref to buffer with encoded data
36             # buf_offset - offset in buffer
37             # int_ref - ref to scalar where result will be stored
38             # N - bits in first byte
39             #
40             # returns: count of readed bytes of encoded integer
41             # or undef on error (malformed data)
42              
43             sub int_decode {
44 214     214 0 39664 my ( $buf_ref, $buf_offset, $int_ref, $N ) = @_;
45 214 50       38591 return undef if length($$buf_ref) - $buf_offset <= 0;
46 214   50     38734 $N ||= 7;
47 214         38516 my $ff = ( 1 << $N ) - 1;
48              
49 214         38565 $$int_ref = $ff & vec( $$buf_ref, $buf_offset, 8 );
50 214 100       76931 return 1 if $$int_ref < $ff;
51              
52 1         3 my $l = length($$buf_ref) - $buf_offset - 1;
53              
54 1         3 for my $i ( 1 .. $l ) {
55 2 50       6 return undef if $i > MAX_INT_SIZE;
56 2         5 my $s = vec( $$buf_ref, $i + $buf_offset, 8 );
57 2         5 $$int_ref += ( $s & 0x7f ) << ( $i - 1 ) * 7;
58 2 100       10 return $i + 1 if $s < 0x80;
59             }
60              
61 0         0 return undef;
62             }
63              
64             sub str_encode {
65 76     76 0 14821 my $str = shift;
66 76         13711 my $huff_str = huffman_encode($str);
67 76         13550 my $pack;
68 76 100       13596 if ( length($huff_str) < length($str) ) {
69 66         12312 $pack = int_encode( length($huff_str), 7 );
70 66         12243 vec( $pack, 7, 1 ) = 1;
71 66         24498 $pack .= $huff_str;
72             }
73             else {
74 10         1369 $pack = int_encode( length($str), 7 );
75 10         2670 $pack .= $str;
76             }
77 76         27016 return $pack;
78             }
79              
80             # str_decode()
81             # arguments:
82             # buf_ref - ref to buffer with encoded data
83             # buf_offset - offset in buffer
84             # str_ref - ref to scalar where result will be stored
85             # returns: count of readed bytes of encoded data
86              
87             sub str_decode {
88 72     72 0 14906 my ( $buf_ref, $buf_offset, $str_ref ) = @_;
89 72         13814 my $offset = int_decode( $buf_ref, $buf_offset, \my $l, 7 );
90             return undef
91 72 50 33     13895 unless defined $offset
92             && length($$buf_ref) - $buf_offset - $offset >= $l;
93              
94 72         13751 $$str_ref = substr $$buf_ref, $offset + $buf_offset, $l;
95 72 100       13816 $$str_ref = huffman_decode($$str_ref)
96             if vec( $$buf_ref, $buf_offset * 8 + 7, 1 ) == 1;
97 72         27166 return $offset + $l;
98             }
99              
100             sub evict_ht {
101 144     144 0 27328 my ( $context, $size ) = @_;
102 144         27043 my @evicted;
103              
104 144         27260 my $ht = $context->{header_table};
105              
106 144         27804 while ( $context->{ht_size} + $size >
107             $context->{settings}->{&SETTINGS_HEADER_TABLE_SIZE} )
108             {
109 5         6 my $n = $#$ht;
110 5         8 my $kv_ref = pop @$ht;
111             $context->{ht_size} -=
112 5         10 32 + length( $kv_ref->[0] ) + length( $kv_ref->[1] );
113 5         15 tracer->debug( sprintf "Evicted header [%i] %s = %s\n",
114             $n + 1, @$kv_ref );
115 5         28 push @evicted, [ $n, @$kv_ref ];
116             }
117 144         54311 return @evicted;
118             }
119              
120             sub add_to_ht {
121 144     144 0 27409 my ( $context, $key, $value ) = @_;
122 144         27322 my $size = length($key) + length($value) + 32;
123 144 50       27611 return () if $size > $context->{settings}->{&SETTINGS_HEADER_TABLE_SIZE};
124              
125 144         27468 my @evicted = evict_ht( $context, $size );
126              
127 144         27302 my $ht = $context->{header_table};
128 144         27217 my $kv_ref = [ $key, $value ];
129              
130 144         27530 unshift @$ht, $kv_ref;
131 144         27448 $context->{ht_size} += $size;
132 144         67957 return @evicted;
133             }
134              
135             sub headers_decode {
136 33     33 0 5691 my ( $con, $buf_ref, $buf_offset, $length, $stream_id ) = @_;
137              
138 33         5552 my $context = $con->decode_context;
139              
140 33         5564 my $ht = $context->{header_table};
141 33         5483 my $eh = $context->{emitted_headers};
142              
143 33         5518 my $offset = 0;
144              
145 33         5612 while ( $offset < $length ) {
146              
147 142         24803 my $f = vec( $$buf_ref, $buf_offset + $offset, 8 );
148 142         24791 tracer->debug("\toffset: $offset\n");
149              
150             # Indexed Header
151 142 100 66     25637 if ( $f & 0x80 ) {
    100 66        
    50 33        
    0 33        
152 72         10998 my $size =
153             int_decode( $buf_ref, $buf_offset + $offset, \my $index, 7 );
154 72 50       10985 return $offset unless $size;
155              
156             # DECODING ERROR
157 72 50       10936 if ( $index == 0 ) {
158 0         0 tracer->error("Indexed header with zero index\n");
159 0         0 $con->error(COMPRESSION_ERROR);
160 0         0 return undef;
161             }
162              
163 72         11046 tracer->debug("\tINDEXED($index) HEADER\t");
164              
165             # Static table or Header Table entry
166 72 100       11008 if ( $index <= @stable ) {
    50          
167 64         10989 my ( $key, $value ) = @{ $stable[ $index - 1 ] };
  64         21833  
168 64         11054 push @$eh, $key, $value;
169 64         11106 tracer->debug("$key = $value\n");
170             }
171             elsif ( $index > @stable + @$ht ) {
172 0         0 tracer->error(
173             "Indexed header with index out of header table: "
174             . $index
175             . "\n" );
176 0         0 $con->error(COMPRESSION_ERROR);
177 0         0 return undef;
178             }
179             else {
180 8         20 my $kv_ref = $ht->[ $index - @stable - 1 ];
181              
182 8         15 push @$eh, @$kv_ref;
183 8         19 tracer->debug("$kv_ref->[0] = $kv_ref->[1]\n");
184             }
185              
186 72         22026 $offset += $size;
187             }
188              
189             # Literal Header Field - New Name
190             elsif ( $f == 0x40 || $f == 0x00 || $f == 0x10 ) {
191 1         4 my $key_size =
192             str_decode( $buf_ref, $buf_offset + $offset + 1, \my $key );
193 1 50       5 return $offset unless $key_size;
194              
195 1 50       4 if ( $key_size == 1 ) {
196 0         0 tracer->error("Empty literal header name");
197 0         0 $con->error(COMPRESSION_ERROR);
198 0         0 return undef;
199             }
200              
201 1 50 33     6 if ( $key =~ /[^a-z0-9\!\#\$\%\&\'\*\+\-\^\_\`]/ && $key !~ /^\:/ )
202             {
203 0         0 tracer->warning("Illegal characters in header name");
204 0         0 $con->stream_error( $stream_id, PROTOCOL_ERROR );
205 0         0 return undef;
206             }
207              
208 1         4 my $value_size =
209             str_decode( $buf_ref, $buf_offset + $offset + 1 + $key_size,
210             \my $value );
211 1 50       4 return $offset unless $value_size;
212              
213             # Emitting header
214 1         3 push @$eh, $key, $value;
215              
216             # Add to index
217 1 50       3 if ( $f == 0x40 ) {
218 1         4 add_to_ht( $context, $key, $value );
219             }
220 1         4 tracer->debug( sprintf "\tLITERAL(new) HEADER\t%s: %s\n",
221             $key, substr( $value, 0, 30 ) );
222              
223 1         5 $offset += 1 + $key_size + $value_size;
224             }
225              
226             # Literal Header Field - Indexed Name
227             elsif (( $f & 0xC0 ) == 0x40
228             || ( $f & 0xF0 ) == 0x00
229             || ( $f & 0xF0 ) == 0x10 )
230             {
231 69 50       13816 my $size = int_decode( $buf_ref, $buf_offset + $offset,
232             \my $index, ( $f & 0xC0 ) == 0x40 ? 6 : 4 );
233 69 50       13687 return $offset unless $size;
234              
235 69         13885 my $value_size =
236             str_decode( $buf_ref, $buf_offset + $offset + $size, \my $value );
237 69 50       13907 return $offset unless $value_size;
238              
239 69         13490 my $key;
240              
241 69 50       13797 if ( $index <= @stable ) {
    0          
242 69         27407 $key = $stable[ $index - 1 ]->[0];
243             }
244             elsif ( $index > @stable + @$ht ) {
245 0         0 tracer->error(
246             "Literal header with index out of header table: "
247             . $index
248             . "\n" );
249 0         0 $con->error(COMPRESSION_ERROR);
250 0         0 return undef;
251             }
252             else {
253 0         0 $key = $ht->[ $index - @stable - 1 ]->[0];
254             }
255              
256             # Emitting header
257 69         13675 push @$eh, $key, $value;
258              
259             # Add to index
260 69 50       13869 if ( ( $f & 0xC0 ) == 0x40 ) {
261 69         13602 add_to_ht( $context, $key, $value );
262             }
263 69         13789 tracer->debug("\tLITERAL($index) HEADER\t$key: $value\n");
264              
265 69         33075 $offset += $size + $value_size;
266             }
267              
268             # Encoding Context Update - Maximum Header Table Size change
269             elsif ( ( $f & 0xE0 ) == 0x20 ) {
270 0         0 my $size =
271             int_decode( $buf_ref, $buf_offset + $offset, \my $ht_size, 5 );
272 0 0       0 return $offset unless $size;
273              
274             # It's not possible to increase size of HEADER_TABLE
275 0 0       0 if (
276             $ht_size > $context->{settings}->{&SETTINGS_HEADER_TABLE_SIZE} )
277             {
278             tracer->error( "Peer attempt to increase "
279             . "SETTINGS_HEADER_TABLE_SIZE higher than current size: "
280             . "$ht_size > "
281 0         0 . $context->{settings}->{&SETTINGS_HEADER_TABLE_SIZE} );
282 0         0 $con->error(COMPRESSION_ERROR);
283 0         0 return undef;
284             }
285 0         0 $context->{settings}->{&SETTINGS_HEADER_TABLE_SIZE} = $ht_size;
286 0         0 evict_ht( $context, 0 );
287 0         0 $offset += $size;
288             }
289              
290             # Encoding Error
291             else {
292 0         0 tracer->error( sprintf( "Unknown header type: %08b", $f ) );
293 0         0 $con->error(COMPRESSION_ERROR);
294 0         0 return undef;
295             }
296             }
297 33         10957 return $offset;
298             }
299              
300             sub headers_encode {
301 36     36 0 7539 my ( $context, $headers ) = @_;
302 36         5536 my $res = '';
303 36         5447 my $ht = $context->{header_table};
304              
305             HLOOP:
306 36         5532 for my $n ( 0 .. $#$headers / 2 ) {
307 156         24729 my $header = lc( $headers->[ 2 * $n ] );
308 156         24695 my $value = $headers->[ 2 * $n + 1 ];
309 156         24549 my $hdr;
310              
311 156         24838 tracer->debug("Encoding header: $header = $value\n");
312              
313 156         24861 for my $i ( 0 .. $#$ht ) {
314             next
315 181 100 100     44226 unless $ht->[$i]->[0] eq $header
316             && $ht->[$i]->[1] eq $value;
317 13         31 $hdr = int_encode( $i + @stable + 1, 7 );
318 13         32 vec( $hdr, 7, 1 ) = 1;
319 13         22 $res .= $hdr;
320 13         33 tracer->debug(
321             "\talready in header table, index " . ( $i + 1 ) . "\n" );
322 13         34 next HLOOP;
323             }
324              
325             # 7.1 Indexed header field representation
326 143 100       24979 if ( exists $rstable{ $header . ' ' . $value } ) {
    100          
327 69         11178 $hdr = int_encode( $rstable{ $header . ' ' . $value }, 7 );
328 69         11106 vec( $hdr, 7, 1 ) = 1;
329             tracer->debug( "\tIndexed header "
330 69         11063 . $rstable{ $header . ' ' . $value }
331             . " from table\n" );
332             }
333              
334             # 7.2.1 Literal Header Field with Incremental Indexing
335             # (Indexed Name)
336             elsif ( exists $rstable{ $header . ' ' } ) {
337 73         13949 $hdr = int_encode( $rstable{ $header . ' ' }, 6 );
338 73         13726 vec( $hdr, 3, 2 ) = 1;
339 73         13609 $hdr .= str_encode($value);
340 73         13609 add_to_ht( $context, $header, $value );
341             tracer->debug( "\tLiteral header "
342 73         13735 . $rstable{ $header . ' ' }
343             . " indexed name\n" );
344             }
345              
346             # 7.2.1 Literal Header Field with Incremental Indexing
347             # (New Name)
348             else {
349 1         3 $hdr = pack( 'C', 0x40 );
350 1         3 $hdr .= str_encode($header) . str_encode($value);
351 1         4 add_to_ht( $context, $header, $value );
352 1         3 tracer->debug("\tLiteral header new name\n");
353             }
354              
355 143         30219 $res .= $hdr;
356             }
357              
358 36         10910 return $res;
359             }
360              
361             1;