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 10     10   42 use strict;
  10         10  
  10         322  
3 10     10   45 use warnings;
  10         13  
  10         232  
4 10     10   3577 use Protocol::HTTP2::Huffman;
  10         23  
  10         589  
5 10     10   4302 use Protocol::HTTP2::StaticTable;
  10         21  
  10         1069  
6 10     10   57 use Protocol::HTTP2::Constants qw(:errors :settings :limits);
  10         15  
  10         2998  
7 10     10   56 use Protocol::HTTP2::Trace qw(tracer bin2hex);
  10         15  
  10         487  
8 10     10   47 use Exporter qw(import);
  10         13  
  10         19659  
9             our @EXPORT_OK = qw(int_encode int_decode str_encode str_decode headers_decode
10             headers_encode);
11              
12             sub int_encode {
13 213     213 0 30565 my ( $int, $N ) = @_;
14 213   50     28200 $N ||= 7;
15 213         28880 my $ff = ( 1 << $N ) - 1;
16              
17 213 100       28322 if ( $int < $ff ) {
18 210         56948 return pack 'C', $int;
19             }
20              
21 3         9 my $res = pack 'C', $ff;
22 3         4 $int -= $ff;
23              
24 3         8 while ( $int >= 0x80 ) {
25 1         4 $res .= pack( 'C', ( $int & 0x7f ) | 0x80 );
26 1         3 $int >>= 7;
27             }
28              
29 3         11 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 191     191 0 31026 my ( $buf_ref, $buf_offset, $int_ref, $N ) = @_;
45 191 50       29849 return undef if length($$buf_ref) - $buf_offset <= 0;
46 191   50     29515 $N ||= 7;
47 191         29369 my $ff = ( 1 << $N ) - 1;
48              
49 191         29212 $$int_ref = $ff & vec( $$buf_ref, $buf_offset, 8 );
50 191 100       58699 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       4 return undef if $i > MAX_INT_SIZE;
56 2         4 my $s = vec( $$buf_ref, $i + $buf_offset, 8 );
57 2         4 $$int_ref += ( $s & 0x7f ) << ( $i - 1 ) * 7;
58 2 100       9 return $i + 1 if $s < 0x80;
59             }
60              
61 0         0 return undef;
62             }
63              
64             sub str_encode {
65 71     71 0 11403 my $str = shift;
66 71         10572 my $huff_str = huffman_encode($str);
67 71         9775 my $pack;
68 71 100       9900 if ( length($huff_str) < length($str) ) {
69 63         9327 $pack = int_encode( length($huff_str), 7 );
70 63         9005 vec( $pack, 7, 1 ) = 1;
71 63         18060 $pack .= $huff_str;
72             }
73             else {
74 8         1032 $pack = int_encode( length($str), 7 );
75 8         1996 $pack .= $str;
76             }
77 71         19896 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 67     67 0 10797 my ( $buf_ref, $buf_offset, $str_ref ) = @_;
89 67         10164 my $offset = int_decode( $buf_ref, $buf_offset, \my $l, 7 );
90             return undef
91 67 50 33     9990 unless defined $offset
92             && length($$buf_ref) - $buf_offset - $offset >= $l;
93              
94 67         10132 $$str_ref = substr $$buf_ref, $offset + $buf_offset, $l;
95 67 100       10169 $$str_ref = huffman_decode($$str_ref)
96             if vec( $$buf_ref, $buf_offset * 8 + 7, 1 ) == 1;
97 67         21010 return $offset + $l;
98             }
99              
100             sub evict_ht {
101 134     134 0 20188 my ( $context, $size ) = @_;
102 134         19861 my @evicted;
103              
104 134         19934 my $ht = $context->{header_table};
105              
106 134         20775 while ( $context->{ht_size} + $size >
107             $context->{settings}->{&SETTINGS_HEADER_TABLE_SIZE} )
108             {
109 5         5 my $n = $#$ht;
110 5         5 my $kv_ref = pop @$ht;
111 5         8 $context->{ht_size} -=
112             32 + length( $kv_ref->[0] ) + length( $kv_ref->[1] );
113 5         10 tracer->debug( sprintf "Evicted header [%i] %s = %s\n",
114             $n + 1, @$kv_ref );
115 5         21 push @evicted, [ $n, @$kv_ref ];
116             }
117 134         40094 return @evicted;
118             }
119              
120             sub add_to_ht {
121 134     134 0 20940 my ( $context, $key, $value ) = @_;
122 134         20406 my $size = length($key) + length($value) + 32;
123 134 50       20589 return () if $size > $context->{settings}->{&SETTINGS_HEADER_TABLE_SIZE};
124              
125 134         20487 my @evicted = evict_ht( $context, $size );
126              
127 134         20387 my $ht = $context->{header_table};
128 134         20638 my $kv_ref = [ $key, $value ];
129              
130 134         20443 unshift @$ht, $kv_ref;
131 134         20234 $context->{ht_size} += $size;
132 134         51510 return @evicted;
133             }
134              
135             sub headers_decode {
136 27     27 0 4124 my ( $con, $buf_ref, $buf_offset, $length, $stream_id ) = @_;
137              
138 27         4060 my $context = $con->decode_context;
139              
140 27         4275 my $ht = $context->{header_table};
141 27         3985 my $eh = $context->{emitted_headers};
142              
143 27         4067 my $offset = 0;
144              
145 27         4060 while ( $offset < $length ) {
146              
147 124         18970 my $f = vec( $$buf_ref, $buf_offset + $offset, 8 );
148 124         19469 tracer->debug("\toffset: $offset\n");
149              
150             # Indexed Header
151 124 100 66     19793 if ( $f & 0x80 ) {
    100 66        
    50 33        
    0 33        
152 59         9351 my $size =
153             int_decode( $buf_ref, $buf_offset + $offset, \my $index, 7 );
154 59 50       9485 return $offset unless $size;
155              
156             # DECODING ERROR
157 59 50       9219 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 59         9211 tracer->debug("\tINDEXED($index) HEADER\t");
164              
165             # Static table or Header Table entry
166 59 100       8455 if ( $index <= @stable ) {
    50          
167 57         8201 my ( $key, $value ) = @{ $stable[ $index - 1 ] };
  57         16590  
168 57         8474 push @$eh, $key, $value;
169 57         8207 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 2         4 my $kv_ref = $ht->[ $index - @stable - 1 ];
181              
182 2         4 push @$eh, @$kv_ref;
183 2         3 tracer->debug("$kv_ref->[0] = $kv_ref->[1]\n");
184             }
185              
186 59         17965 $offset += $size;
187             }
188              
189             # Literal Header Field - New Name
190             elsif ( $f == 0x40 || $f == 0x00 || $f == 0x10 ) {
191 1         3 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     7 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         5 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         3 add_to_ht( $context, $key, $value );
219             }
220 1         3 tracer->debug( sprintf "\tLITERAL(new) HEADER\t%s: %s\n",
221             $key, substr( $value, 0, 30 ) );
222              
223 1         4 $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 64 50       10195 my $size = int_decode( $buf_ref, $buf_offset + $offset,
232             \my $index, ( $f & 0xC0 ) == 0x40 ? 6 : 4 );
233 64 50       10060 return $offset unless $size;
234              
235 64         9968 my $value_size =
236             str_decode( $buf_ref, $buf_offset + $offset + $size, \my $value );
237 64 50       10495 return $offset unless $value_size;
238              
239 64         10362 my $key;
240              
241 64 50       10470 if ( $index <= @stable ) {
    0          
242 64         20538 $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 64         10023 push @$eh, $key, $value;
258              
259             # Add to index
260 64 50       10185 if ( ( $f & 0xC0 ) == 0x40 ) {
261 64         10939 add_to_ht( $context, $key, $value );
262             }
263 64         10467 tracer->debug("\tLITERAL($index) HEADER\t$key: $value\n");
264              
265 64         24141 $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 0         0 tracer->error( "Peer attempt to increase "
279             . "SETTINGS_HEADER_TABLE_SIZE higher than current size: "
280             . "$ht_size > "
281             . $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 27         7980 return $offset;
298             }
299              
300             sub headers_encode {
301 30     30 0 5841 my ( $context, $headers ) = @_;
302 30         3944 my $res = '';
303 30         4060 my $ht = $context->{header_table};
304              
305             HLOOP:
306 30         4108 for my $n ( 0 .. $#$headers / 2 ) {
307 138         18728 my $header = lc( $headers->[ 2 * $n ] );
308 138         17885 my $value = $headers->[ 2 * $n + 1 ];
309 138         18025 my $hdr;
310              
311 138         18915 tracer->debug("Encoding header: $header = $value\n");
312              
313 138         18313 for my $i ( 0 .. $#$ht ) {
314             next
315 155 100 100     32949 unless $ht->[$i]->[0] eq $header
316             && $ht->[$i]->[1] eq $value;
317 7         15 $hdr = int_encode( $i + @stable + 1, 7 );
318 7         13 vec( $hdr, 7, 1 ) = 1;
319 7         10 $res .= $hdr;
320 7         14 tracer->debug(
321             "\talready in header table, index " . ( $i + 1 ) . "\n" );
322 7         14 next HLOOP;
323             }
324              
325             # 7.1 Indexed header field representation
326 131 100       18539 if ( exists $rstable{ $header . ' ' . $value } ) {
    100          
327 62         8150 $hdr = int_encode( $rstable{ $header . ' ' . $value }, 7 );
328 62         8157 vec( $hdr, 7, 1 ) = 1;
329 62         7896 tracer->debug( "\tIndexed header "
330             . $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 68         10139 $hdr = int_encode( $rstable{ $header . ' ' }, 6 );
338 68         10170 vec( $hdr, 3, 2 ) = 1;
339 68         10313 $hdr .= str_encode($value);
340 68         10268 add_to_ht( $context, $header, $value );
341 68         10304 tracer->debug( "\tLiteral header "
342             . $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         2 $hdr = pack( 'C', 0x40 );
350 1         2 $hdr .= str_encode($header) . str_encode($value);
351 1         3 add_to_ht( $context, $header, $value );
352 1         3 tracer->debug("\tLiteral header new name\n");
353             }
354              
355 131         22670 $res .= $hdr;
356             }
357              
358 30         8061 return $res;
359             }
360              
361             1;