blob: 3cddcd0133d3ccd7fcecac0d68f529dc9c110079 [file] [log] [blame]
//
// ========================================================================
// 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);
}
}
}
}