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  
91      public HpackEncoder()
92      {
93          this(4096,4096);
94      }
95  
96      public HpackEncoder(int localMaxDynamicTableSize)
97      {
98          this(localMaxDynamicTableSize,4096);
99      }
100 
101     public HpackEncoder(int localMaxDynamicTableSize,int remoteMaxDynamicTableSize)
102     {
103         _context=new HpackContext(remoteMaxDynamicTableSize);
104         _remoteMaxDynamicTableSize=remoteMaxDynamicTableSize;
105         _localMaxDynamicTableSize=localMaxDynamicTableSize;
106         _debug=LOG.isDebugEnabled();
107     }
108 
109     public HpackContext getHpackContext()
110     {
111         return _context;
112     }
113 
114     public void setRemoteMaxDynamicTableSize(int remoteMaxDynamicTableSize)
115     {
116         _remoteMaxDynamicTableSize=remoteMaxDynamicTableSize;
117     }
118 
119     public void setLocalMaxDynamicTableSize(int localMaxDynamicTableSize)
120     {
121         _localMaxDynamicTableSize=localMaxDynamicTableSize;
122     }
123 
124     public void encode(ByteBuffer buffer, MetaData metadata)
125     {
126         if (LOG.isDebugEnabled())
127             LOG.debug(String.format("CtxTbl[%x] encoding",_context.hashCode()));
128 
129         int pos = buffer.position();
130 
131         // Check the dynamic table sizes!
132         int maxDynamicTableSize=Math.min(_remoteMaxDynamicTableSize,_localMaxDynamicTableSize);
133         if (maxDynamicTableSize!=_context.getMaxDynamicTableSize())
134             encodeMaxDynamicTableSize(buffer,maxDynamicTableSize);
135 
136         // Add Request/response meta fields
137         if (metadata.isRequest())
138         {
139             MetaData.Request request = (MetaData.Request)metadata;
140 
141             // TODO optimise these to avoid HttpField creation
142             String scheme=request.getURI().getScheme();
143             encode(buffer,new HttpField(HttpHeader.C_SCHEME,scheme==null?HttpScheme.HTTP.asString():scheme));
144             encode(buffer,new HttpField(HttpHeader.C_METHOD,request.getMethod()));
145             encode(buffer,new HttpField(HttpHeader.C_AUTHORITY,request.getURI().getAuthority()));
146             encode(buffer,new HttpField(HttpHeader.C_PATH,request.getURI().getPathQuery()));
147 
148         }
149         else if (metadata.isResponse())
150         {
151             MetaData.Response response = (MetaData.Response)metadata;
152             int code=response.getStatus();
153             HttpField status = code<__status.length?__status[code]:null;
154             if (status==null)
155                 status=new HttpField.IntValueHttpField(HttpHeader.C_STATUS,code);
156             encode(buffer,status);
157         }
158 
159         // Add all the other fields
160         for (HttpField field : metadata)
161             encode(buffer,field);
162 
163         if (LOG.isDebugEnabled())
164             LOG.debug(String.format("CtxTbl[%x] encoded %d octets",_context.hashCode(), buffer.position() - pos));
165     }
166 
167     public void encodeMaxDynamicTableSize(ByteBuffer buffer, int maxDynamicTableSize)
168     {
169         if (maxDynamicTableSize>_remoteMaxDynamicTableSize)
170             throw new IllegalArgumentException();
171         buffer.put((byte)0x20);
172         NBitInteger.encode(buffer,5,maxDynamicTableSize);
173         _context.resize(maxDynamicTableSize);
174     }
175 
176     public void encode(ByteBuffer buffer, HttpField field)
177     {
178         if (field.getValue()==null)
179             field = new HttpField(field.getHeader(),field.getName(),"");
180         
181         final int p=_debug?buffer.position():-1;
182 
183         String encoding=null;
184 
185         // Is there an entry for the field?
186         Entry entry = _context.get(field);
187         if (entry!=null)
188         {
189             // Known field entry, so encode it as indexed
190             if (entry.isStatic())
191             {
192                 buffer.put(((StaticEntry)entry).getEncodedField());
193                 if (_debug)
194                     encoding="IdxFieldS1";
195             }
196             else
197             {
198                 int index=_context.index(entry);
199                 buffer.put((byte)0x80);
200                 NBitInteger.encode(buffer,7,index);
201                 if (_debug)
202                     encoding="IdxField"+(entry.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(7,index));
203             }
204         }
205         else
206         {
207             // Unknown field entry, so we will have to send literally.
208             final boolean indexed;
209 
210             // But do we know it's name?
211             HttpHeader header = field.getHeader();
212 
213             // Select encoding strategy
214             if (header==null)
215             {
216                 // Select encoding strategy for unknown header names
217                 Entry name = _context.get(field.getName());
218 
219                 if (field instanceof PreEncodedHttpField)
220                 {
221                     int i=buffer.position();
222                     ((PreEncodedHttpField)field).putTo(buffer,HttpVersion.HTTP_2);
223                     byte b=buffer.get(i);
224                     indexed=b<0||b>=0x40;
225                     if (_debug)
226                         encoding=indexed?"PreEncodedIdx":"PreEncoded";
227                 }
228                 // has the custom header name been seen before?
229                 else if (name==null)
230                 {
231                     // unknown name and value, so let's index this just in case it is
232                     // the first time we have seen a custom name or a custom field.
233                     // unless the name is changing, this is worthwhile
234                     indexed=true;
235                     encodeName(buffer,(byte)0x40,6,field.getName(),null);
236                     encodeValue(buffer,true,field.getValue());
237                     if (_debug)
238                         encoding="LitHuffNHuffVIdx";
239                 }
240                 else
241                 {
242                     // known custom name, but unknown value.
243                     // This is probably a custom field with changing value, so don't index.
244                     indexed=false;
245                     encodeName(buffer,(byte)0x00,4,field.getName(),null);
246                     encodeValue(buffer,true,field.getValue());
247                     if (_debug)
248                         encoding="LitHuffNHuffV!Idx";
249                 }
250             }
251             else
252             {
253                 // Select encoding strategy for known header names
254                 Entry name = _context.get(header);
255 
256                 if (field instanceof PreEncodedHttpField)
257                 {
258                     // Preencoded field
259                     int i=buffer.position();
260                     ((PreEncodedHttpField)field).putTo(buffer,HttpVersion.HTTP_2);
261                     byte b=buffer.get(i);
262                     indexed=b<0||b>=0x40;
263                     if (_debug)
264                         encoding=indexed?"PreEncodedIdx":"PreEncoded";
265                 }
266                 else if (__DO_NOT_INDEX.contains(header))
267                 {
268                     // Non indexed field
269                     indexed=false;
270                     boolean never_index=__NEVER_INDEX.contains(header);
271                     boolean huffman=!__DO_NOT_HUFFMAN.contains(header);
272                     encodeName(buffer,never_index?(byte)0x10:(byte)0x00,4,header.asString(),name);
273                     encodeValue(buffer,huffman,field.getValue());
274 
275                     if (_debug)
276                         encoding="Lit"+
277                                 ((name==null)?"HuffN":("IdxN"+(name.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(4,_context.index(name)))))+
278                                 (huffman?"HuffV":"LitV")+
279                                 (indexed?"Idx":(never_index?"!!Idx":"!Idx"));
280                 }
281                 else if (header==HttpHeader.CONTENT_LENGTH && field.getValue().length()>1)
282                 {
283                     // Non indexed content length for 2 digits or more
284                     indexed=false;
285                     encodeName(buffer,(byte)0x00,4,header.asString(),name);
286                     encodeValue(buffer,true,field.getValue());
287                     if (_debug)
288                         encoding="LitIdxNS"+(1+NBitInteger.octectsNeeded(4,_context.index(name)))+"HuffV!Idx";
289                 }
290                 else
291                 {
292                     // indexed
293                     indexed=true;
294                     boolean huffman=!__DO_NOT_HUFFMAN.contains(header);
295                     encodeName(buffer,(byte)0x40,6,header.asString(),name);
296                     encodeValue(buffer,huffman,field.getValue());
297                     if (_debug)
298                         encoding=((name==null)?"LitHuffN":("LitIdxN"+(name.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(6,_context.index(name)))))+
299                                 (huffman?"HuffVIdx":"LitVIdx");
300                 }
301             }
302 
303             // If we want the field referenced, then we add it to our
304             // table and reference set.
305             if (indexed)
306                 _context.add(field);
307         }
308 
309         if (_debug)
310         {
311             int e=buffer.position();
312             if (LOG.isDebugEnabled())
313                 LOG.debug("encode {}:'{}' to '{}'",encoding,field,TypeUtil.toHexString(buffer.array(),buffer.arrayOffset()+p,e-p));
314         }
315     }
316 
317     private void encodeName(ByteBuffer buffer, byte mask, int bits, String name, Entry entry)
318     {
319         buffer.put(mask);
320         if (entry==null)
321         {
322             // leave name index bits as 0
323             // Encode the name always with lowercase huffman
324             buffer.put((byte)0x80);
325             NBitInteger.encode(buffer,7,Huffman.octetsNeededLC(name));
326             Huffman.encodeLC(buffer,name);
327         }
328         else
329         {
330             NBitInteger.encode(buffer,bits,_context.index(entry));
331         }
332     }
333 
334     static void encodeValue(ByteBuffer buffer, boolean huffman, String value)
335     {
336         if (huffman)
337         {
338             // huffman literal value
339             buffer.put((byte)0x80);
340             NBitInteger.encode(buffer,7,Huffman.octetsNeeded(value));
341             Huffman.encode(buffer,value);
342         }
343         else
344         {
345             // add literal assuming iso_8859_1
346             buffer.put((byte)0x00);
347             NBitInteger.encode(buffer,7,value.length());
348             for (int i=0;i<value.length();i++)
349             {
350                 char c=value.charAt(i);
351                 if (c<' '|| c>127)
352                     throw new IllegalArgumentException();
353                 buffer.put((byte)c);
354             }
355         }
356     }
357 }