View Javadoc

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