diff options
Diffstat (limited to 'jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java')
-rw-r--r-- | jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java new file mode 100644 index 0000000000..e42ffb86b5 --- /dev/null +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java @@ -0,0 +1,354 @@ +// +// ======================================================================== +// Copyright (c) 1995-2016 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); + } + } + } +} |