View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.http2.hpack;
20  
21  import java.nio.ByteBuffer;
22  import java.util.HashMap;
23  import java.util.HashSet;
24  import java.util.Map;
25  import java.util.Set;
26  
27  import org.eclipse.jetty.http.HttpField;
28  import org.eclipse.jetty.http.HttpHeader;
29  import org.eclipse.jetty.http.HttpMethod;
30  import org.eclipse.jetty.http.HttpScheme;
31  import org.eclipse.jetty.util.ArrayQueue;
32  import org.eclipse.jetty.util.ArrayTernaryTrie;
33  import org.eclipse.jetty.util.StringUtil;
34  import org.eclipse.jetty.util.Trie;
35  import org.eclipse.jetty.util.log.Log;
36  import org.eclipse.jetty.util.log.Logger;
37  
38  
39  /* ------------------------------------------------------------ */
40  /** HPACK - Header Compression for HTTP/2
41   * <p>This class maintains the compression context for a single HTTP/2
42   * connection. Specifically it holds the static and dynamic Header Field Tables
43   * and the associated sizes and limits.
44   * </p>
45   * <p>It is compliant with draft 11 of the specification</p>
46   */
47  public class HpackContext
48  {
49      public static final Logger LOG = Log.getLogger(HpackContext.class);
50      
51      public static final String[][] STATIC_TABLE = 
52      {
53          {null,null},
54          /* 1  */ {":authority",null},
55          /* 2  */ {":method","GET"},
56          /* 3  */ {":method","POST"},
57          /* 4  */ {":path","/"},
58          /* 5  */ {":path","/index.html"},
59          /* 6  */ {":scheme","http"},
60          /* 7  */ {":scheme","https"},
61          /* 8  */ {":status","200"},
62          /* 9  */ {":status","204"},
63          /* 10 */ {":status","206"},
64          /* 11 */ {":status","304"},
65          /* 12 */ {":status","400"},
66          /* 13 */ {":status","404"},
67          /* 14 */ {":status","500"},
68          /* 15 */ {"accept-charset",null},
69          /* 16 */ {"accept-encoding","gzip, deflate"},
70          /* 17 */ {"accept-language",null},
71          /* 18 */ {"accept-ranges",null},
72          /* 19 */ {"accept",null},
73          /* 20 */ {"access-control-allow-origin",null},
74          /* 21 */ {"age",null},
75          /* 22 */ {"allow",null},
76          /* 23 */ {"authorization",null},
77          /* 24 */ {"cache-control",null},
78          /* 25 */ {"content-disposition",null},
79          /* 26 */ {"content-encoding",null},
80          /* 27 */ {"content-language",null},
81          /* 28 */ {"content-length",null},
82          /* 29 */ {"content-location",null},
83          /* 30 */ {"content-range",null},
84          /* 31 */ {"content-type",null},
85          /* 32 */ {"cookie",null},
86          /* 33 */ {"date",null},
87          /* 34 */ {"etag",null},
88          /* 35 */ {"expect",null},
89          /* 36 */ {"expires",null},
90          /* 37 */ {"from",null},
91          /* 38 */ {"host",null},
92          /* 39 */ {"if-match",null},
93          /* 40 */ {"if-modified-since",null},
94          /* 41 */ {"if-none-match",null},
95          /* 42 */ {"if-range",null},
96          /* 43 */ {"if-unmodified-since",null},
97          /* 44 */ {"last-modified",null},
98          /* 45 */ {"link",null},
99          /* 46 */ {"location",null},
100         /* 47 */ {"max-forwards",null},
101         /* 48 */ {"proxy-authenticate",null},
102         /* 49 */ {"proxy-authorization",null},
103         /* 50 */ {"range",null},
104         /* 51 */ {"referer",null},
105         /* 52 */ {"refresh",null},
106         /* 53 */ {"retry-after",null},
107         /* 54 */ {"server",null},
108         /* 55 */ {"set-cookie",null},
109         /* 56 */ {"strict-transport-security",null},
110         /* 57 */ {"transfer-encoding",null},
111         /* 58 */ {"user-agent",null},
112         /* 59 */ {"vary",null},
113         /* 60 */ {"via",null},
114         /* 61 */ {"www-authenticate",null},
115     };
116     
117     private static final Map<HttpField,Entry> __staticFieldMap = new HashMap<>();
118     private static final Trie<StaticEntry> __staticNameMap = new ArrayTernaryTrie<>(true,512);
119     private static final StaticEntry[] __staticTableByHeader = new StaticEntry[HttpHeader.UNKNOWN.ordinal()];
120     private static final StaticEntry[] __staticTable=new StaticEntry[STATIC_TABLE.length];
121     static
122     {
123         Set<String> added = new HashSet<>();
124         for (int i=1;i<STATIC_TABLE.length;i++)
125         {
126             StaticEntry entry=null;
127 
128             String name  = STATIC_TABLE[i][0];
129             String value = STATIC_TABLE[i][1];
130             HttpHeader header = HttpHeader.CACHE.get(name);
131             if (header!=null && value!=null)
132             {
133                 switch (header)
134                 {
135                     case C_METHOD:
136                     {
137                         
138                         HttpMethod method = HttpMethod.CACHE.get(value);
139                         if (method!=null)
140                             entry=new StaticEntry(i,new StaticTableHttpField(header,name,value,method));
141                         break;
142                     }
143                     
144                     case C_SCHEME:
145                     {
146                         
147                         HttpScheme scheme = HttpScheme.CACHE.get(value);
148                         if (scheme!=null)
149                             entry=new StaticEntry(i,new StaticTableHttpField(header,name,value,scheme));
150                         break;
151                     }
152                     
153                     case C_STATUS:
154                     {
155                         entry=new StaticEntry(i,new StaticTableHttpField(header,name,value,Integer.valueOf(value)));
156                         break;
157                     }
158                     
159                     default:
160                         break;
161                 }
162             }
163             
164             if (entry==null)
165                 entry=new StaticEntry(i,header==null?new HttpField(STATIC_TABLE[i][0],value):new HttpField(header,name,value));
166             
167                         
168             __staticTable[i]=entry;
169             
170             if (entry._field.getValue()!=null)
171                 __staticFieldMap.put(entry._field,entry);
172             
173             if (!added.contains(entry._field.getName()))
174             {
175                 added.add(entry._field.getName());
176                 __staticNameMap.put(entry._field.getName(),entry);
177                 if (__staticNameMap.get(entry._field.getName())==null)
178                     throw new IllegalStateException("name trie too small");
179             }
180         }
181         
182         for (HttpHeader h : HttpHeader.values())
183         {
184             StaticEntry entry = __staticNameMap.get(h.asString());
185             if (entry!=null)
186                 __staticTableByHeader[h.ordinal()]=entry;
187         }
188     }
189     
190     private int _maxDynamicTableSizeInBytes;
191     private int _dynamicTableSizeInBytes;
192     private final DynamicTable _dynamicTable;
193     private final Map<HttpField,Entry> _fieldMap = new HashMap<>();
194     private final Map<String,Entry> _nameMap = new HashMap<>();
195     
196     HpackContext(int maxDynamicTableSize)
197     {
198         _maxDynamicTableSizeInBytes=maxDynamicTableSize;
199         int guesstimateEntries = 10+maxDynamicTableSize/(32+10+10);
200         _dynamicTable=new DynamicTable(guesstimateEntries,guesstimateEntries+10);
201         if (LOG.isDebugEnabled())
202             LOG.debug(String.format("HdrTbl[%x] created max=%d",hashCode(),maxDynamicTableSize));
203     }
204     
205     public void resize(int newMaxDynamicTableSize)
206     {
207         if (LOG.isDebugEnabled())
208             LOG.debug(String.format("HdrTbl[%x] resized max=%d->%d",hashCode(),_maxDynamicTableSizeInBytes,newMaxDynamicTableSize));
209         _maxDynamicTableSizeInBytes=newMaxDynamicTableSize;
210         int guesstimateEntries = 10+newMaxDynamicTableSize/(32+10+10);
211         evict();
212         _dynamicTable.resizeUnsafe(guesstimateEntries);
213     }
214     
215     public Entry get(HttpField field)
216     {
217         Entry entry = _fieldMap.get(field);
218         if (entry==null)
219             entry=__staticFieldMap.get(field);
220         return entry;
221     }
222     
223     public Entry get(String name)
224     {
225         Entry entry = __staticNameMap.get(name);
226         if (entry!=null)
227             return entry;
228         return _nameMap.get(StringUtil.asciiToLowerCase(name));
229     }
230     
231     public Entry get(int index)
232     {
233         if (index<__staticTable.length)
234             return __staticTable[index];
235             
236         int d=_dynamicTable.size()-index+__staticTable.length-1;
237 
238         if (d>=0) 
239             return _dynamicTable.getUnsafe(d);      
240         return null;
241     }
242     
243     public Entry get(HttpHeader header)
244     {
245         Entry e = __staticTableByHeader[header.ordinal()];
246         if (e==null)
247             return get(header.asString());
248         return e;
249     }
250 
251     public static Entry getStatic(HttpHeader header)
252     {
253         return __staticTableByHeader[header.ordinal()];
254     }
255     
256     public Entry add(HttpField field)
257     {
258         int slot=_dynamicTable.getNextSlotUnsafe();
259         Entry entry=new Entry(slot,field);
260         int size = entry.getSize();
261         if (size>_maxDynamicTableSizeInBytes)
262         {
263             if (LOG.isDebugEnabled())
264                 LOG.debug(String.format("HdrTbl[%x] !added size %d>%d",hashCode(),size,_maxDynamicTableSizeInBytes));
265             return null;
266         }
267         _dynamicTableSizeInBytes+=size;
268         _dynamicTable.addUnsafe(entry);
269         _fieldMap.put(field,entry);
270         _nameMap.put(StringUtil.asciiToLowerCase(field.getName()),entry);
271 
272         if (LOG.isDebugEnabled())
273             LOG.debug(String.format("HdrTbl[%x] added %s",hashCode(),entry));
274         evict();
275         return entry;
276     }
277 
278     /**
279      * @return Current dynamic table size in entries
280      */
281     public int size()
282     {
283         return _dynamicTable.size();
284     }
285     
286     /**
287      * @return Current Dynamic table size in Octets
288      */
289     public int getDynamicTableSize()
290     {
291         return _dynamicTableSizeInBytes;
292     }
293 
294     /**
295      * @return Max Dynamic table size in Octets
296      */
297     public int getMaxDynamicTableSize()
298     {
299         return _maxDynamicTableSizeInBytes;
300     }
301 
302     public int index(Entry entry)
303     {
304         if (entry._slot<0)
305             return 0;
306         if (entry.isStatic())
307             return entry._slot;
308 
309         return _dynamicTable.index(entry)+__staticTable.length-1;
310     }
311     
312     public static int staticIndex(HttpHeader header)
313     {
314         if (header==null)
315             return 0;
316         Entry entry=__staticNameMap.get(header.asString());
317         if (entry==null)
318             return 0;
319         return entry.getSlot();
320     }
321     
322     private void evict()
323     {
324         while (_dynamicTableSizeInBytes>_maxDynamicTableSizeInBytes)
325         {
326             Entry entry = _dynamicTable.pollUnsafe();
327             if (LOG.isDebugEnabled())
328                 LOG.debug(String.format("HdrTbl[%x] evict %s",hashCode(),entry));
329             _dynamicTableSizeInBytes-=entry.getSize();
330             entry._slot=-1;
331             _fieldMap.remove(entry.getHttpField());
332             String lc=StringUtil.asciiToLowerCase(entry.getHttpField().getName());
333             if (entry==_nameMap.get(lc))
334                 _nameMap.remove(lc);
335         }
336         if (LOG.isDebugEnabled())
337             LOG.debug(String.format("HdrTbl[%x] entries=%d, size=%d, max=%d",hashCode(),_dynamicTable.size(),_dynamicTableSizeInBytes,_maxDynamicTableSizeInBytes));
338     }
339     
340     @Override
341     public String toString()
342     {
343         return String.format("HpackContext@%x{entries=%d,size=%d,max=%d}",hashCode(),_dynamicTable.size(),_dynamicTableSizeInBytes,_maxDynamicTableSizeInBytes);
344     }
345     
346     
347     
348     /* ------------------------------------------------------------ */
349     /**
350      */
351     private class DynamicTable extends ArrayQueue<HpackContext.Entry>
352     {
353         /* ------------------------------------------------------------ */
354         /**
355          * @param initCapacity
356          * @param growBy
357          */
358         private DynamicTable(int initCapacity, int growBy)
359         {
360             super(initCapacity,growBy);
361         }
362 
363         /* ------------------------------------------------------------ */
364         /**
365          * @see org.eclipse.jetty.util.ArrayQueue#growUnsafe()
366          */
367         @Override
368         protected void resizeUnsafe(int newCapacity)
369         {
370             // Relay on super.growUnsafe to pack all entries 0 to _nextSlot
371             super.resizeUnsafe(newCapacity);
372             for (int s=0;s<_nextSlot;s++)
373                 ((Entry)_elements[s])._slot=s;
374         }
375 
376         /* ------------------------------------------------------------ */
377         /**
378          * @see org.eclipse.jetty.util.ArrayQueue#enqueue(java.lang.Object)
379          */
380         @Override
381         public boolean enqueue(Entry e)
382         {
383             return super.enqueue(e);
384         }
385 
386         /* ------------------------------------------------------------ */
387         /**
388          * @see org.eclipse.jetty.util.ArrayQueue#dequeue()
389          */
390         @Override
391         public Entry dequeue()
392         {
393             return super.dequeue();
394         }
395         
396         /* ------------------------------------------------------------ */
397         /**
398          * @param entry
399          * @return
400          */
401         private int index(Entry entry)
402         {
403             return entry._slot>=_nextE?_size-entry._slot+_nextE:_nextSlot-entry._slot;
404         }
405 
406     }
407 
408 
409 
410     /* ------------------------------------------------------------ */
411     /* ------------------------------------------------------------ */
412     /* ------------------------------------------------------------ */
413     public static class Entry
414     {
415         final HttpField _field;
416         int _slot;
417         
418         Entry()
419         {    
420             _slot=0;
421             _field=null;
422         }
423         
424         Entry(int index,String name, String value)
425         {    
426             _slot=index;
427             _field=new HttpField(name,value);
428         }
429         
430         Entry(int slot, HttpField field)
431         {    
432             _slot=slot;
433             _field=field;
434         }
435 
436         public int getSize()
437         {
438             return 32+_field.getName().length()+_field.getValue().length();
439         }
440         
441         public HttpField getHttpField()
442         {
443             return _field;
444         }
445         
446         public boolean isStatic()
447         {
448             return false;
449         }
450         
451         public byte[] getStaticHuffmanValue()
452         {
453             return null;
454         }
455         
456         public int getSlot()
457         {
458             return _slot;
459         }
460         
461         public String toString()
462         {
463             return String.format("{%s,%d,%s,%x}",isStatic()?"S":"D",_slot,_field,hashCode());
464         }
465     } 
466     
467     public static class StaticEntry extends Entry
468     {
469         private final byte[] _huffmanValue;
470         private final byte _encodedField;
471         
472         StaticEntry(int index,HttpField field)
473         {    
474             super(index,field);
475             String value = field.getValue();
476             if (value!=null && value.length()>0)
477             {
478                 int huffmanLen = Huffman.octetsNeeded(value);
479                 int lenLen = NBitInteger.octectsNeeded(7,huffmanLen);
480                 _huffmanValue = new byte[1+lenLen+huffmanLen];
481                 ByteBuffer buffer = ByteBuffer.wrap(_huffmanValue); 
482                         
483                 // Indicate Huffman
484                 buffer.put((byte)0x80);
485                 // Add huffman length
486                 NBitInteger.encode(buffer,7,huffmanLen);
487                 // Encode value
488                 Huffman.encode(buffer,value);       
489             }
490             else
491                 _huffmanValue=null;
492             
493             _encodedField=(byte)(0x80|index);
494         }
495 
496         @Override
497         public boolean isStatic()
498         {
499             return true;
500         }
501         
502         @Override
503         public byte[] getStaticHuffmanValue()
504         {
505             return _huffmanValue;
506         }
507         
508         public byte getEncodedField()
509         {
510             return _encodedField;
511         }
512     }
513 
514 
515 }