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         final int p=_debug?buffer.position():-1;
179 
180         String encoding=null;
181 
182         // Is there an entry for the field?
183         Entry entry = _context.get(field);
184         if (entry!=null)
185         {
186             // Known field entry, so encode it as indexed
187             if (entry.isStatic())
188             {
189                 buffer.put(((StaticEntry)entry).getEncodedField());
190                 if (_debug)
191                     encoding="IdxFieldS1";
192             }
193             else
194             {
195                 int index=_context.index(entry);
196                 buffer.put((byte)0x80);
197                 NBitInteger.encode(buffer,7,index);
198                 if (_debug)
199                     encoding="IdxField"+(entry.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(7,index));
200             }
201         }
202         else
203         {
204             // Unknown field entry, so we will have to send literally.
205             final boolean indexed;
206 
207             // But do we know it's name?
208             HttpHeader header = field.getHeader();
209 
210             // Select encoding strategy
211             if (header==null)
212             {
213                 // Select encoding strategy for unknown header names
214                 Entry name = _context.get(field.getName());
215 
216                 if (field instanceof PreEncodedHttpField)
217                 {
218                     int i=buffer.position();
219                     ((PreEncodedHttpField)field).putTo(buffer,HttpVersion.HTTP_2);
220                     byte b=buffer.get(i);
221                     indexed=b<0||b>=0x40;
222                     if (_debug)
223                         encoding=indexed?"PreEncodedIdx":"PreEncoded";
224                 }
225                 // has the custom header name been seen before?
226                 else if (name==null)
227                 {
228                     // unknown name and value, so let's index this just in case it is
229                     // the first time we have seen a custom name or a custom field.
230                     // unless the name is changing, this is worthwhile
231                     indexed=true;
232                     encodeName(buffer,(byte)0x40,6,field.getName(),null);
233                     encodeValue(buffer,true,field.getValue());
234                     if (_debug)
235                         encoding="LitHuffNHuffVIdx";
236                 }
237                 else
238                 {
239                     // known custom name, but unknown value.
240                     // This is probably a custom field with changing value, so don't index.
241                     indexed=false;
242                     encodeName(buffer,(byte)0x00,4,field.getName(),null);
243                     encodeValue(buffer,true,field.getValue());
244                     if (_debug)
245                         encoding="LitHuffNHuffV!Idx";
246                 }
247             }
248             else
249             {
250                 // Select encoding strategy for known header names
251                 Entry name = _context.get(header);
252 
253                 if (field instanceof PreEncodedHttpField)
254                 {
255                     // Preencoded field
256                     int i=buffer.position();
257                     ((PreEncodedHttpField)field).putTo(buffer,HttpVersion.HTTP_2);
258                     byte b=buffer.get(i);
259                     indexed=b<0||b>=0x40;
260                     if (_debug)
261                         encoding=indexed?"PreEncodedIdx":"PreEncoded";
262                 }
263                 else if (__DO_NOT_INDEX.contains(header))
264                 {
265                     // Non indexed field
266                     indexed=false;
267                     boolean never_index=__NEVER_INDEX.contains(header);
268                     boolean huffman=!__DO_NOT_HUFFMAN.contains(header);
269                     encodeName(buffer,never_index?(byte)0x10:(byte)0x00,4,header.asString(),name);
270                     encodeValue(buffer,huffman,field.getValue());
271 
272                     if (_debug)
273                         encoding="Lit"+
274                                 ((name==null)?"HuffN":("IdxN"+(name.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(4,_context.index(name)))))+
275                                 (huffman?"HuffV":"LitV")+
276                                 (indexed?"Idx":(never_index?"!!Idx":"!Idx"));
277                 }
278                 else if (header==HttpHeader.CONTENT_LENGTH && field.getValue().length()>1)
279                 {
280                     // Non indexed content length for 2 digits or more
281                     indexed=false;
282                     encodeName(buffer,(byte)0x00,4,header.asString(),name);
283                     encodeValue(buffer,true,field.getValue());
284                     if (_debug)
285                         encoding="LitIdxNS"+(1+NBitInteger.octectsNeeded(4,_context.index(name)))+"HuffV!Idx";
286                 }
287                 else
288                 {
289                     // indexed
290                     indexed=true;
291                     boolean huffman=!__DO_NOT_HUFFMAN.contains(header);
292                     encodeName(buffer,(byte)0x40,6,header.asString(),name);
293                     encodeValue(buffer,huffman,field.getValue());
294                     if (_debug)
295                         encoding=((name==null)?"LitHuffN":("LitIdxN"+(name.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(6,_context.index(name)))))+
296                                 (huffman?"HuffVIdx":"LitVIdx");
297                 }
298             }
299 
300             // If we want the field referenced, then we add it to our
301             // table and reference set.
302             if (indexed)
303                 _context.add(field);
304         }
305 
306         if (_debug)
307         {
308             int e=buffer.position();
309             if (LOG.isDebugEnabled())
310                 LOG.debug("encode {}:'{}' to '{}'",encoding,field,TypeUtil.toHexString(buffer.array(),buffer.arrayOffset()+p,e-p));
311         }
312     }
313 
314     private void encodeName(ByteBuffer buffer, byte mask, int bits, String name, Entry entry)
315     {
316         buffer.put(mask);
317         if (entry==null)
318         {
319             // leave name index bits as 0
320             // Encode the name always with lowercase huffman
321             buffer.put((byte)0x80);
322             NBitInteger.encode(buffer,7,Huffman.octetsNeededLC(name));
323             Huffman.encodeLC(buffer,name);
324         }
325         else
326         {
327             NBitInteger.encode(buffer,bits,_context.index(entry));
328         }
329     }
330 
331     static void encodeValue(ByteBuffer buffer, boolean huffman, String value)
332     {
333         if (huffman)
334         {
335             // huffman literal value
336             buffer.put((byte)0x80);
337             NBitInteger.encode(buffer,7,Huffman.octetsNeeded(value));
338             Huffman.encode(buffer,value);
339         }
340         else
341         {
342             // add literal assuming iso_8859_1
343             buffer.put((byte)0x00);
344             NBitInteger.encode(buffer,7,value.length());
345             for (int i=0;i<value.length();i++)
346             {
347                 char c=value.charAt(i);
348                 if (c<' '|| c>127)
349                     throw new IllegalArgumentException();
350                 buffer.put((byte)c);
351             }
352         }
353     }
354 }