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  
20  package org.eclipse.jetty.http2.hpack;
21  
22  import java.nio.ByteBuffer;
23  import java.util.EnumSet;
24  
25  import org.eclipse.jetty.http.HttpField;
26  import org.eclipse.jetty.http.HttpHeader;
27  import org.eclipse.jetty.http.HttpScheme;
28  import org.eclipse.jetty.http.HttpStatus;
29  import org.eclipse.jetty.http.HttpVersion;
30  import org.eclipse.jetty.http.MetaData;
31  import org.eclipse.jetty.http.PreEncodedHttpField;
32  import org.eclipse.jetty.http2.hpack.HpackContext.Entry;
33  import org.eclipse.jetty.http2.hpack.HpackContext.StaticEntry;
34  import org.eclipse.jetty.util.TypeUtil;
35  import org.eclipse.jetty.util.log.Log;
36  import org.eclipse.jetty.util.log.Logger;
37  
38  public class HpackEncoder
39  {
40      public static final Logger LOG = Log.getLogger(HpackEncoder.class);
41  
42      private final static HttpField[] __status= new HttpField[599];
43  
44  
45      final static EnumSet<HttpHeader> __DO_NOT_HUFFMAN =
46              EnumSet.of(
47                      HttpHeader.AUTHORIZATION,
48                      HttpHeader.CONTENT_MD5,
49                      HttpHeader.PROXY_AUTHENTICATE,
50                      HttpHeader.PROXY_AUTHORIZATION);
51  
52      final static EnumSet<HttpHeader> __DO_NOT_INDEX =
53              EnumSet.of(
54                      // HttpHeader.C_PATH,  // TODO more data needed
55                      // HttpHeader.DATE,    // TODO more data needed
56                      HttpHeader.AUTHORIZATION,
57                      HttpHeader.CONTENT_MD5,
58                      HttpHeader.CONTENT_RANGE,
59                      HttpHeader.ETAG,
60                      HttpHeader.IF_MODIFIED_SINCE,
61                      HttpHeader.IF_UNMODIFIED_SINCE,
62                      HttpHeader.IF_NONE_MATCH,
63                      HttpHeader.IF_RANGE,
64                      HttpHeader.IF_MATCH,
65                      HttpHeader.LOCATION,
66                      HttpHeader.RANGE,
67                      HttpHeader.RETRY_AFTER,
68                      // HttpHeader.EXPIRES,
69                      HttpHeader.LAST_MODIFIED,
70                      HttpHeader.SET_COOKIE,
71                      HttpHeader.SET_COOKIE2);
72  
73  
74      final static EnumSet<HttpHeader> __NEVER_INDEX =
75              EnumSet.of(
76                      HttpHeader.AUTHORIZATION,
77                      HttpHeader.SET_COOKIE,
78                      HttpHeader.SET_COOKIE2);
79  
80      static
81      {
82          for (HttpStatus.Code code : HttpStatus.Code.values())
83              __status[code.getCode()]=new PreEncodedHttpField(HttpHeader.C_STATUS,Integer.toString(code.getCode()));
84      }
85  
86      private final HpackContext _context;
87      private final boolean _debug;
88      private int _remoteMaxDynamicTableSize;
89      private int _localMaxDynamicTableSize;
90      private int _maxHeaderListSize;
91      private int _size;
92  
93      public HpackEncoder()
94      {
95          this(4096,4096,-1);
96      }
97  
98      public HpackEncoder(int localMaxDynamicTableSize)
99      {
100         this(localMaxDynamicTableSize,4096,-1);
101     }
102     
103     public HpackEncoder(int localMaxDynamicTableSize,int remoteMaxDynamicTableSize)
104     {
105         this(localMaxDynamicTableSize,remoteMaxDynamicTableSize,-1);
106     }
107     
108     public HpackEncoder(int localMaxDynamicTableSize,int remoteMaxDynamicTableSize, int maxHeaderListSize)
109     {
110         _context=new HpackContext(remoteMaxDynamicTableSize);
111         _remoteMaxDynamicTableSize=remoteMaxDynamicTableSize;
112         _localMaxDynamicTableSize=localMaxDynamicTableSize;
113         _maxHeaderListSize=maxHeaderListSize;
114         _debug=LOG.isDebugEnabled();
115     }
116 
117     public int getMaxHeaderListSize()
118     {
119         return _maxHeaderListSize;
120     }
121 
122     public void setMaxHeaderListSize(int maxHeaderListSize)
123     {
124         _maxHeaderListSize = maxHeaderListSize;
125     }
126 
127     public HpackContext getHpackContext()
128     {
129         return _context;
130     }
131 
132     public void setRemoteMaxDynamicTableSize(int remoteMaxDynamicTableSize)
133     {
134         _remoteMaxDynamicTableSize=remoteMaxDynamicTableSize;
135     }
136 
137     public void setLocalMaxDynamicTableSize(int localMaxDynamicTableSize)
138     {
139         _localMaxDynamicTableSize=localMaxDynamicTableSize;
140     }
141 
142     public void encode(ByteBuffer buffer, MetaData metadata)
143     {
144         if (LOG.isDebugEnabled())
145             LOG.debug(String.format("CtxTbl[%x] encoding",_context.hashCode()));
146 
147         _size=0;
148         int pos = buffer.position();
149 
150         // Check the dynamic table sizes!
151         int maxDynamicTableSize=Math.min(_remoteMaxDynamicTableSize,_localMaxDynamicTableSize);
152         if (maxDynamicTableSize!=_context.getMaxDynamicTableSize())
153             encodeMaxDynamicTableSize(buffer,maxDynamicTableSize);
154 
155         // Add Request/response meta fields
156         if (metadata.isRequest())
157         {
158             MetaData.Request request = (MetaData.Request)metadata;
159 
160             // TODO optimise these to avoid HttpField creation
161             String scheme=request.getURI().getScheme();
162             encode(buffer,new HttpField(HttpHeader.C_SCHEME,scheme==null?HttpScheme.HTTP.asString():scheme));
163             encode(buffer,new HttpField(HttpHeader.C_METHOD,request.getMethod()));
164             encode(buffer,new HttpField(HttpHeader.C_AUTHORITY,request.getURI().getAuthority()));
165             encode(buffer,new HttpField(HttpHeader.C_PATH,request.getURI().getPathQuery()));
166         }
167         else if (metadata.isResponse())
168         {
169             MetaData.Response response = (MetaData.Response)metadata;
170             int code=response.getStatus();
171             HttpField status = code<__status.length?__status[code]:null;
172             if (status==null)
173                 status=new HttpField.IntValueHttpField(HttpHeader.C_STATUS,code);
174             encode(buffer,status);
175         }
176 
177         // Add all the other fields
178         for (HttpField field : metadata)
179             encode(buffer,field);
180 
181         // Check size
182         if (_maxHeaderListSize>0 && _size>_maxHeaderListSize)
183         {
184             LOG.warn("Header list size too large {} > {} for {}",_size,_maxHeaderListSize);
185             if (LOG.isDebugEnabled())
186                 LOG.debug("metadata={}",metadata);
187         }
188         
189         if (LOG.isDebugEnabled())
190             LOG.debug(String.format("CtxTbl[%x] encoded %d octets",_context.hashCode(), buffer.position() - pos));
191     }
192 
193     public void encodeMaxDynamicTableSize(ByteBuffer buffer, int maxDynamicTableSize)
194     {
195         if (maxDynamicTableSize>_remoteMaxDynamicTableSize)
196             throw new IllegalArgumentException();
197         buffer.put((byte)0x20);
198         NBitInteger.encode(buffer,5,maxDynamicTableSize);
199         _context.resize(maxDynamicTableSize);
200     }
201 
202     public void encode(ByteBuffer buffer, HttpField field)
203     {
204         if (field.getValue()==null)
205             field = new HttpField(field.getHeader(),field.getName(),"");
206         
207         int field_size = field.getName().length() + field.getValue().length();
208         _size+=field_size+32;
209         
210         final int p=_debug?buffer.position():-1;
211 
212         String encoding=null;
213 
214         // Is there an entry for the field?
215         Entry entry = _context.get(field);
216         if (entry!=null)
217         {
218             // Known field entry, so encode it as indexed
219             if (entry.isStatic())
220             {
221                 buffer.put(((StaticEntry)entry).getEncodedField());
222                 if (_debug)
223                     encoding="IdxFieldS1";
224             }
225             else
226             {
227                 int index=_context.index(entry);
228                 buffer.put((byte)0x80);
229                 NBitInteger.encode(buffer,7,index);
230                 if (_debug)
231                     encoding="IdxField"+(entry.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(7,index));
232             }
233         }
234         else
235         {
236             // Unknown field entry, so we will have to send literally.
237             final boolean indexed;
238 
239             // But do we know it's name?
240             HttpHeader header = field.getHeader();
241 
242             // Select encoding strategy
243             if (header==null)
244             {
245                 // Select encoding strategy for unknown header names
246                 Entry name = _context.get(field.getName());
247 
248                 if (field instanceof PreEncodedHttpField)
249                 {
250                     int i=buffer.position();
251                     ((PreEncodedHttpField)field).putTo(buffer,HttpVersion.HTTP_2);
252                     byte b=buffer.get(i);
253                     indexed=b<0||b>=0x40;
254                     if (_debug)
255                         encoding=indexed?"PreEncodedIdx":"PreEncoded";
256                 }
257                 // has the custom header name been seen before?
258                 else if (name==null)
259                 {
260                     // unknown name and value, so let's index this just in case it is
261                     // the first time we have seen a custom name or a custom field.
262                     // unless the name is changing, this is worthwhile
263                     indexed=true;
264                     encodeName(buffer,(byte)0x40,6,field.getName(),null);
265                     encodeValue(buffer,true,field.getValue());
266                     if (_debug)
267                         encoding="LitHuffNHuffVIdx";
268                 }
269                 else
270                 {
271                     // known custom name, but unknown value.
272                     // This is probably a custom field with changing value, so don't index.
273                     indexed=false;
274                     encodeName(buffer,(byte)0x00,4,field.getName(),null);
275                     encodeValue(buffer,true,field.getValue());
276                     if (_debug)
277                         encoding="LitHuffNHuffV!Idx";
278                 }
279             }
280             else
281             {
282                 // Select encoding strategy for known header names
283                 Entry name = _context.get(header);
284 
285                 if (field instanceof PreEncodedHttpField)
286                 {
287                     // Preencoded field
288                     int i=buffer.position();
289                     ((PreEncodedHttpField)field).putTo(buffer,HttpVersion.HTTP_2);
290                     byte b=buffer.get(i);
291                     indexed=b<0||b>=0x40;
292                     if (_debug)
293                         encoding=indexed?"PreEncodedIdx":"PreEncoded";
294                 }
295                 else if (__DO_NOT_INDEX.contains(header))
296                 {
297                     // Non indexed field
298                     indexed=false;
299                     boolean never_index=__NEVER_INDEX.contains(header);
300                     boolean huffman=!__DO_NOT_HUFFMAN.contains(header);
301                     encodeName(buffer,never_index?(byte)0x10:(byte)0x00,4,header.asString(),name);
302                     encodeValue(buffer,huffman,field.getValue());
303 
304                     if (_debug)
305                         encoding="Lit"+
306                                 ((name==null)?"HuffN":("IdxN"+(name.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(4,_context.index(name)))))+
307                                 (huffman?"HuffV":"LitV")+
308                                 (indexed?"Idx":(never_index?"!!Idx":"!Idx"));
309                 }
310                 else if (header==HttpHeader.CONTENT_LENGTH && field.getValue().length()>1)
311                 {
312                     // Non indexed content length for 2 digits or more
313                     indexed=false;
314                     encodeName(buffer,(byte)0x00,4,header.asString(),name);
315                     encodeValue(buffer,true,field.getValue());
316                     if (_debug)
317                         encoding="LitIdxNS"+(1+NBitInteger.octectsNeeded(4,_context.index(name)))+"HuffV!Idx";
318                 }
319                 else
320                 {
321                     // indexed
322                     indexed=true;
323                     boolean huffman=!__DO_NOT_HUFFMAN.contains(header);
324                     encodeName(buffer,(byte)0x40,6,header.asString(),name);
325                     encodeValue(buffer,huffman,field.getValue());
326                     if (_debug)
327                         encoding=((name==null)?"LitHuffN":("LitIdxN"+(name.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(6,_context.index(name)))))+
328                                 (huffman?"HuffVIdx":"LitVIdx");
329                 }
330             }
331 
332             // If we want the field referenced, then we add it to our
333             // table and reference set.
334             if (indexed)
335                 _context.add(field);
336         }
337 
338         if (_debug)
339         {
340             int e=buffer.position();
341             if (LOG.isDebugEnabled())
342                 LOG.debug("encode {}:'{}' to '{}'",encoding,field,TypeUtil.toHexString(buffer.array(),buffer.arrayOffset()+p,e-p));
343         }
344     }
345 
346     private void encodeName(ByteBuffer buffer, byte mask, int bits, String name, Entry entry)
347     {
348         buffer.put(mask);
349         if (entry==null)
350         {
351             // leave name index bits as 0
352             // Encode the name always with lowercase huffman
353             buffer.put((byte)0x80);
354             NBitInteger.encode(buffer,7,Huffman.octetsNeededLC(name));
355             Huffman.encodeLC(buffer,name);
356         }
357         else
358         {
359             NBitInteger.encode(buffer,bits,_context.index(entry));
360         }
361     }
362 
363     static void encodeValue(ByteBuffer buffer, boolean huffman, String value)
364     {
365         if (huffman)
366         {
367             // huffman literal value
368             buffer.put((byte)0x80);
369             NBitInteger.encode(buffer,7,Huffman.octetsNeeded(value));
370             Huffman.encode(buffer,value);
371         }
372         else
373         {
374             // add literal assuming iso_8859_1
375             buffer.put((byte)0x00);
376             NBitInteger.encode(buffer,7,value.length());
377             for (int i=0;i<value.length();i++)
378             {
379                 char c=value.charAt(i);
380                 if (c<' '|| c>127)
381                     throw new IllegalArgumentException();
382                 buffer.put((byte)c);
383             }
384         }
385     }
386 }