| // |
| // ======================================================================== |
| // Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| |
| package org.eclipse.jetty.http2.hpack; |
| |
| import java.nio.ByteBuffer; |
| import java.util.EnumSet; |
| |
| import org.eclipse.jetty.http.HttpField; |
| import org.eclipse.jetty.http.HttpHeader; |
| import org.eclipse.jetty.http.HttpScheme; |
| import org.eclipse.jetty.http.HttpStatus; |
| import org.eclipse.jetty.http.HttpVersion; |
| import org.eclipse.jetty.http.MetaData; |
| import org.eclipse.jetty.http.PreEncodedHttpField; |
| import org.eclipse.jetty.http2.hpack.HpackContext.Entry; |
| import org.eclipse.jetty.http2.hpack.HpackContext.StaticEntry; |
| import org.eclipse.jetty.util.TypeUtil; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| |
| public class HpackEncoder |
| { |
| public static final Logger LOG = Log.getLogger(HpackEncoder.class); |
| |
| private final static HttpField[] __status= new HttpField[599]; |
| |
| |
| final static EnumSet<HttpHeader> __DO_NOT_HUFFMAN = |
| EnumSet.of( |
| HttpHeader.AUTHORIZATION, |
| HttpHeader.CONTENT_MD5, |
| HttpHeader.PROXY_AUTHENTICATE, |
| HttpHeader.PROXY_AUTHORIZATION); |
| |
| final static EnumSet<HttpHeader> __DO_NOT_INDEX = |
| EnumSet.of( |
| // HttpHeader.C_PATH, // TODO more data needed |
| // HttpHeader.DATE, // TODO more data needed |
| HttpHeader.AUTHORIZATION, |
| HttpHeader.CONTENT_MD5, |
| HttpHeader.CONTENT_RANGE, |
| HttpHeader.ETAG, |
| HttpHeader.IF_MODIFIED_SINCE, |
| HttpHeader.IF_UNMODIFIED_SINCE, |
| HttpHeader.IF_NONE_MATCH, |
| HttpHeader.IF_RANGE, |
| HttpHeader.IF_MATCH, |
| HttpHeader.LOCATION, |
| HttpHeader.RANGE, |
| HttpHeader.RETRY_AFTER, |
| // HttpHeader.EXPIRES, |
| HttpHeader.LAST_MODIFIED, |
| HttpHeader.SET_COOKIE, |
| HttpHeader.SET_COOKIE2); |
| |
| |
| final static EnumSet<HttpHeader> __NEVER_INDEX = |
| EnumSet.of( |
| HttpHeader.AUTHORIZATION, |
| HttpHeader.SET_COOKIE, |
| HttpHeader.SET_COOKIE2); |
| |
| static |
| { |
| for (HttpStatus.Code code : HttpStatus.Code.values()) |
| __status[code.getCode()]=new PreEncodedHttpField(HttpHeader.C_STATUS,Integer.toString(code.getCode())); |
| } |
| |
| private final HpackContext _context; |
| private final boolean _debug; |
| private int _remoteMaxDynamicTableSize; |
| private int _localMaxDynamicTableSize; |
| |
| public HpackEncoder() |
| { |
| this(4096,4096); |
| } |
| |
| public HpackEncoder(int localMaxDynamicTableSize) |
| { |
| this(localMaxDynamicTableSize,4096); |
| } |
| |
| public HpackEncoder(int localMaxDynamicTableSize,int remoteMaxDynamicTableSize) |
| { |
| _context=new HpackContext(remoteMaxDynamicTableSize); |
| _remoteMaxDynamicTableSize=remoteMaxDynamicTableSize; |
| _localMaxDynamicTableSize=localMaxDynamicTableSize; |
| _debug=LOG.isDebugEnabled(); |
| } |
| |
| public HpackContext getHpackContext() |
| { |
| return _context; |
| } |
| |
| public void setRemoteMaxDynamicTableSize(int remoteMaxDynamicTableSize) |
| { |
| _remoteMaxDynamicTableSize=remoteMaxDynamicTableSize; |
| } |
| |
| public void setLocalMaxDynamicTableSize(int localMaxDynamicTableSize) |
| { |
| _localMaxDynamicTableSize=localMaxDynamicTableSize; |
| } |
| |
| public void encode(ByteBuffer buffer, MetaData metadata) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug(String.format("CtxTbl[%x] encoding",_context.hashCode())); |
| |
| int pos = buffer.position(); |
| |
| // Check the dynamic table sizes! |
| int maxDynamicTableSize=Math.min(_remoteMaxDynamicTableSize,_localMaxDynamicTableSize); |
| if (maxDynamicTableSize!=_context.getMaxDynamicTableSize()) |
| encodeMaxDynamicTableSize(buffer,maxDynamicTableSize); |
| |
| // Add Request/response meta fields |
| if (metadata.isRequest()) |
| { |
| MetaData.Request request = (MetaData.Request)metadata; |
| |
| // TODO optimise these to avoid HttpField creation |
| String scheme=request.getURI().getScheme(); |
| encode(buffer,new HttpField(HttpHeader.C_SCHEME,scheme==null?HttpScheme.HTTP.asString():scheme)); |
| encode(buffer,new HttpField(HttpHeader.C_METHOD,request.getMethod())); |
| encode(buffer,new HttpField(HttpHeader.C_AUTHORITY,request.getURI().getAuthority())); |
| encode(buffer,new HttpField(HttpHeader.C_PATH,request.getURI().getPathQuery())); |
| |
| } |
| else if (metadata.isResponse()) |
| { |
| MetaData.Response response = (MetaData.Response)metadata; |
| int code=response.getStatus(); |
| HttpField status = code<__status.length?__status[code]:null; |
| if (status==null) |
| status=new HttpField.IntValueHttpField(HttpHeader.C_STATUS,code); |
| encode(buffer,status); |
| } |
| |
| // Add all the other fields |
| for (HttpField field : metadata) |
| encode(buffer,field); |
| |
| if (LOG.isDebugEnabled()) |
| LOG.debug(String.format("CtxTbl[%x] encoded %d octets",_context.hashCode(), buffer.position() - pos)); |
| } |
| |
| public void encodeMaxDynamicTableSize(ByteBuffer buffer, int maxDynamicTableSize) |
| { |
| if (maxDynamicTableSize>_remoteMaxDynamicTableSize) |
| throw new IllegalArgumentException(); |
| buffer.put((byte)0x20); |
| NBitInteger.encode(buffer,5,maxDynamicTableSize); |
| _context.resize(maxDynamicTableSize); |
| } |
| |
| public void encode(ByteBuffer buffer, HttpField field) |
| { |
| final int p=_debug?buffer.position():-1; |
| |
| String encoding=null; |
| |
| // Is there an entry for the field? |
| Entry entry = _context.get(field); |
| if (entry!=null) |
| { |
| // Known field entry, so encode it as indexed |
| if (entry.isStatic()) |
| { |
| buffer.put(((StaticEntry)entry).getEncodedField()); |
| if (_debug) |
| encoding="IdxFieldS1"; |
| } |
| else |
| { |
| int index=_context.index(entry); |
| buffer.put((byte)0x80); |
| NBitInteger.encode(buffer,7,index); |
| if (_debug) |
| encoding="IdxField"+(entry.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(7,index)); |
| } |
| } |
| else |
| { |
| // Unknown field entry, so we will have to send literally. |
| final boolean indexed; |
| |
| // But do we know it's name? |
| HttpHeader header = field.getHeader(); |
| |
| // Select encoding strategy |
| if (header==null) |
| { |
| // Select encoding strategy for unknown header names |
| Entry name = _context.get(field.getName()); |
| |
| if (field instanceof PreEncodedHttpField) |
| { |
| int i=buffer.position(); |
| ((PreEncodedHttpField)field).putTo(buffer,HttpVersion.HTTP_2); |
| byte b=buffer.get(i); |
| indexed=b<0||b>=0x40; |
| if (_debug) |
| encoding=indexed?"PreEncodedIdx":"PreEncoded"; |
| } |
| // has the custom header name been seen before? |
| else if (name==null) |
| { |
| // unknown name and value, so let's index this just in case it is |
| // the first time we have seen a custom name or a custom field. |
| // unless the name is changing, this is worthwhile |
| indexed=true; |
| encodeName(buffer,(byte)0x40,6,field.getName(),null); |
| encodeValue(buffer,true,field.getValue()); |
| if (_debug) |
| encoding="LitHuffNHuffVIdx"; |
| } |
| else |
| { |
| // known custom name, but unknown value. |
| // This is probably a custom field with changing value, so don't index. |
| indexed=false; |
| encodeName(buffer,(byte)0x00,4,field.getName(),null); |
| encodeValue(buffer,true,field.getValue()); |
| if (_debug) |
| encoding="LitHuffNHuffV!Idx"; |
| } |
| } |
| else |
| { |
| // Select encoding strategy for known header names |
| Entry name = _context.get(header); |
| |
| if (field instanceof PreEncodedHttpField) |
| { |
| // Preencoded field |
| int i=buffer.position(); |
| ((PreEncodedHttpField)field).putTo(buffer,HttpVersion.HTTP_2); |
| byte b=buffer.get(i); |
| indexed=b<0||b>=0x40; |
| if (_debug) |
| encoding=indexed?"PreEncodedIdx":"PreEncoded"; |
| } |
| else if (__DO_NOT_INDEX.contains(header)) |
| { |
| // Non indexed field |
| indexed=false; |
| boolean never_index=__NEVER_INDEX.contains(header); |
| boolean huffman=!__DO_NOT_HUFFMAN.contains(header); |
| encodeName(buffer,never_index?(byte)0x10:(byte)0x00,4,header.asString(),name); |
| encodeValue(buffer,huffman,field.getValue()); |
| |
| if (_debug) |
| encoding="Lit"+ |
| ((name==null)?"HuffN":("IdxN"+(name.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(4,_context.index(name)))))+ |
| (huffman?"HuffV":"LitV")+ |
| (indexed?"Idx":(never_index?"!!Idx":"!Idx")); |
| } |
| else if (header==HttpHeader.CONTENT_LENGTH && field.getValue().length()>1) |
| { |
| // Non indexed content length for 2 digits or more |
| indexed=false; |
| encodeName(buffer,(byte)0x00,4,header.asString(),name); |
| encodeValue(buffer,true,field.getValue()); |
| if (_debug) |
| encoding="LitIdxNS"+(1+NBitInteger.octectsNeeded(4,_context.index(name)))+"HuffV!Idx"; |
| } |
| else |
| { |
| // indexed |
| indexed=true; |
| boolean huffman=!__DO_NOT_HUFFMAN.contains(header); |
| encodeName(buffer,(byte)0x40,6,header.asString(),name); |
| encodeValue(buffer,huffman,field.getValue()); |
| if (_debug) |
| encoding=((name==null)?"LitHuffN":("LitIdxN"+(name.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(6,_context.index(name)))))+ |
| (huffman?"HuffVIdx":"LitVIdx"); |
| } |
| } |
| |
| // If we want the field referenced, then we add it to our |
| // table and reference set. |
| if (indexed) |
| _context.add(field); |
| } |
| |
| if (_debug) |
| { |
| int e=buffer.position(); |
| if (LOG.isDebugEnabled()) |
| LOG.debug("encode {}:'{}' to '{}'",encoding,field,TypeUtil.toHexString(buffer.array(),buffer.arrayOffset()+p,e-p)); |
| } |
| } |
| |
| private void encodeName(ByteBuffer buffer, byte mask, int bits, String name, Entry entry) |
| { |
| buffer.put(mask); |
| if (entry==null) |
| { |
| // leave name index bits as 0 |
| // Encode the name always with lowercase huffman |
| buffer.put((byte)0x80); |
| NBitInteger.encode(buffer,7,Huffman.octetsNeededLC(name)); |
| Huffman.encodeLC(buffer,name); |
| } |
| else |
| { |
| NBitInteger.encode(buffer,bits,_context.index(entry)); |
| } |
| } |
| |
| static void encodeValue(ByteBuffer buffer, boolean huffman, String value) |
| { |
| if (huffman) |
| { |
| // huffman literal value |
| buffer.put((byte)0x80); |
| NBitInteger.encode(buffer,7,Huffman.octetsNeeded(value)); |
| Huffman.encode(buffer,value); |
| } |
| else |
| { |
| // add literal assuming iso_8859_1 |
| buffer.put((byte)0x00); |
| NBitInteger.encode(buffer,7,value.length()); |
| for (int i=0;i<value.length();i++) |
| { |
| char c=value.charAt(i); |
| if (c<' '|| c>127) |
| throw new IllegalArgumentException(); |
| buffer.put((byte)c); |
| } |
| } |
| } |
| } |