Issue #185 Implement RFC 7239
Also handle legacy Https headers for #834
This commit is contained in:
parent
c033a3734b
commit
b45af1a3c9
|
@ -34,8 +34,8 @@ public class QuotedCSV implements Iterable<String>
|
|||
{
|
||||
private enum State { VALUE, PARAM_NAME, PARAM_VALUE};
|
||||
|
||||
private final List<String> _values = new ArrayList<>();
|
||||
private final boolean _keepQuotes;
|
||||
protected final List<String> _values = new ArrayList<>();
|
||||
protected final boolean _keepQuotes;
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public QuotedCSV(String... values)
|
||||
|
@ -62,6 +62,10 @@ public class QuotedCSV implements Iterable<String>
|
|||
boolean sloshed=false;
|
||||
int nws_length=0;
|
||||
int last_length=0;
|
||||
int value_length=-1;
|
||||
int param_name=-1;
|
||||
int param_value=-1;
|
||||
|
||||
for (int i=0;i<=l;i++)
|
||||
{
|
||||
char c=i==l?0:value.charAt(i);
|
||||
|
@ -105,12 +109,27 @@ public class QuotedCSV implements Iterable<String>
|
|||
case '"':
|
||||
quoted=true;
|
||||
if (_keepQuotes)
|
||||
{
|
||||
if (state==State.PARAM_VALUE && param_value<0)
|
||||
param_value=nws_length;
|
||||
buffer.append(c);
|
||||
}
|
||||
else if (state==State.PARAM_VALUE && param_value<0)
|
||||
param_value=nws_length;
|
||||
nws_length=buffer.length();
|
||||
continue;
|
||||
|
||||
case ';':
|
||||
buffer.setLength(nws_length); // trim following OWS
|
||||
if (state==State.VALUE)
|
||||
{
|
||||
parsedValue(buffer);
|
||||
value_length=buffer.length();
|
||||
}
|
||||
else
|
||||
parsedParam(buffer,value_length,param_name,param_value);
|
||||
nws_length=buffer.length();
|
||||
param_name=param_value=-1;
|
||||
buffer.append(c);
|
||||
last_length=++nws_length;
|
||||
state=State.PARAM_NAME;
|
||||
|
@ -121,14 +140,54 @@ public class QuotedCSV implements Iterable<String>
|
|||
if (nws_length>0)
|
||||
{
|
||||
buffer.setLength(nws_length); // trim following OWS
|
||||
switch(state)
|
||||
{
|
||||
case VALUE:
|
||||
parsedValue(buffer);
|
||||
value_length=buffer.length();
|
||||
break;
|
||||
case PARAM_NAME:
|
||||
case PARAM_VALUE:
|
||||
parsedParam(buffer,value_length,param_name,param_value);
|
||||
break;
|
||||
}
|
||||
_values.add(buffer.toString());
|
||||
}
|
||||
buffer.setLength(0);
|
||||
last_length=0;
|
||||
nws_length=0;
|
||||
value_length=param_name=param_value=-1;
|
||||
state=State.VALUE;
|
||||
continue;
|
||||
|
||||
case '=':
|
||||
switch (state)
|
||||
{
|
||||
case VALUE:
|
||||
// It wasn't really a value, it was a param name
|
||||
value_length=param_name=0;
|
||||
buffer.setLength(nws_length); // trim following OWS
|
||||
buffer.append(c);
|
||||
last_length=++nws_length;
|
||||
state=State.PARAM_VALUE;
|
||||
continue;
|
||||
|
||||
case PARAM_NAME:
|
||||
buffer.setLength(nws_length); // trim following OWS
|
||||
buffer.append(c);
|
||||
last_length=++nws_length;
|
||||
state=State.PARAM_VALUE;
|
||||
continue;
|
||||
|
||||
case PARAM_VALUE:
|
||||
if (param_value<0)
|
||||
param_value=nws_length;
|
||||
buffer.append(c);
|
||||
nws_length=buffer.length();
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
|
||||
default:
|
||||
{
|
||||
switch (state)
|
||||
|
@ -142,22 +201,17 @@ public class QuotedCSV implements Iterable<String>
|
|||
|
||||
case PARAM_NAME:
|
||||
{
|
||||
if (c=='=')
|
||||
{
|
||||
buffer.setLength(nws_length); // trim following OWS
|
||||
buffer.append(c);
|
||||
last_length=++nws_length;
|
||||
state=State.PARAM_VALUE;
|
||||
continue;
|
||||
}
|
||||
if (param_name<0)
|
||||
param_name=nws_length;
|
||||
buffer.append(c);
|
||||
nws_length=buffer.length();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
case PARAM_VALUE:
|
||||
{
|
||||
if (param_value<0)
|
||||
param_value=nws_length;
|
||||
buffer.append(c);
|
||||
nws_length=buffer.length();
|
||||
continue;
|
||||
|
@ -168,6 +222,26 @@ public class QuotedCSV implements Iterable<String>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a value has been parsed
|
||||
* @param buffer Containing the trimmed value, which may be mutated
|
||||
*/
|
||||
protected void parsedValue(StringBuffer buffer)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a parameter has been parsed
|
||||
* @param buffer Containing the trimmed value and all parameters, which may be mutated
|
||||
* @param valueLength The length of the value
|
||||
* @param paramName The index of the start of the parameter just parsed
|
||||
* @param paramValue The index of the start of the parameter value just parsed, or -1
|
||||
*/
|
||||
protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public List<String> getValues()
|
||||
{
|
||||
return _values;
|
||||
|
|
|
@ -32,13 +32,11 @@ import java.util.List;
|
|||
* @see "https://tools.ietf.org/html/rfc7230#section-7"
|
||||
* @see "https://tools.ietf.org/html/rfc7231#section-5.3.1"
|
||||
*/
|
||||
public class QuotedQualityCSV implements Iterable<String>
|
||||
public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
|
||||
{
|
||||
private final static Double ZERO=new Double(0.0);
|
||||
private final static Double ONE=new Double(1.0);
|
||||
private enum State { VALUE, PARAM_NAME, PARAM_VALUE, Q_VALUE};
|
||||
|
||||
private final List<String> _values = new ArrayList<>();
|
||||
private final List<Double> _quality = new ArrayList<>();
|
||||
private boolean _sorted = false;
|
||||
|
||||
|
@ -53,154 +51,40 @@ public class QuotedQualityCSV implements Iterable<String>
|
|||
/* ------------------------------------------------------------ */
|
||||
public void addValue(String value)
|
||||
{
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
|
||||
int l=value.length();
|
||||
State state=State.VALUE;
|
||||
boolean quoted=false;
|
||||
boolean sloshed=false;
|
||||
int nws_length=0;
|
||||
int last_length=0;
|
||||
Double q=ONE;
|
||||
for (int i=0;i<=l;i++)
|
||||
super.addValue(value);
|
||||
while(_quality.size()<_values.size())
|
||||
_quality.add(ONE);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
@Override
|
||||
protected void parsedValue(StringBuffer buffer)
|
||||
{
|
||||
super.parsedValue(buffer);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
@Override
|
||||
protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
|
||||
{
|
||||
if (buffer.charAt(paramName)=='q' && paramValue>paramName && buffer.charAt(paramName+1)=='=')
|
||||
{
|
||||
char c=i==l?0:value.charAt(i);
|
||||
|
||||
// Handle quoting https://tools.ietf.org/html/rfc7230#section-3.2.6
|
||||
if (quoted && c!=0)
|
||||
Double q;
|
||||
try
|
||||
{
|
||||
if (sloshed)
|
||||
sloshed=false;
|
||||
else
|
||||
{
|
||||
switch(c)
|
||||
{
|
||||
case '\\':
|
||||
sloshed=true;
|
||||
break;
|
||||
case '"':
|
||||
quoted=false;
|
||||
if (state==State.Q_VALUE)
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
buffer.append(c);
|
||||
nws_length=buffer.length();
|
||||
continue;
|
||||
q=(_keepQuotes && buffer.charAt(paramValue)=='"')
|
||||
?new Double(buffer.substring(paramValue+1,buffer.length()-1))
|
||||
:new Double(buffer.substring(paramValue));
|
||||
}
|
||||
|
||||
// Handle common cases
|
||||
switch(c)
|
||||
catch(Exception e)
|
||||
{
|
||||
case ' ':
|
||||
case '\t':
|
||||
if (buffer.length()>last_length) // not leading OWS
|
||||
buffer.append(c);
|
||||
continue;
|
||||
|
||||
case '"':
|
||||
quoted=true;
|
||||
if (state==State.Q_VALUE)
|
||||
continue;
|
||||
|
||||
buffer.append(c);
|
||||
nws_length=buffer.length();
|
||||
continue;
|
||||
|
||||
case ';':
|
||||
if (state==State.Q_VALUE)
|
||||
{
|
||||
try
|
||||
{
|
||||
q=new Double(buffer.substring(last_length));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
q=ZERO;
|
||||
}
|
||||
nws_length=last_length;
|
||||
}
|
||||
|
||||
buffer.setLength(nws_length); // trim following OWS
|
||||
buffer.append(c);
|
||||
last_length=++nws_length;
|
||||
state=State.PARAM_NAME;
|
||||
continue;
|
||||
|
||||
case ',':
|
||||
case 0:
|
||||
if (state==State.Q_VALUE)
|
||||
{
|
||||
try
|
||||
{
|
||||
q=new Double(buffer.substring(last_length));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
q=ZERO;
|
||||
}
|
||||
nws_length=last_length;
|
||||
}
|
||||
buffer.setLength(nws_length); // trim following OWS
|
||||
if (q>0.0 && nws_length>0)
|
||||
{
|
||||
_values.add(buffer.toString());
|
||||
_quality.add(q);
|
||||
_sorted=false;
|
||||
}
|
||||
buffer.setLength(0);
|
||||
last_length=0;
|
||||
nws_length=0;
|
||||
q=ONE;
|
||||
state=State.VALUE;
|
||||
continue;
|
||||
|
||||
default:
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case VALUE:
|
||||
{
|
||||
buffer.append(c);
|
||||
nws_length=buffer.length();
|
||||
continue;
|
||||
}
|
||||
|
||||
case PARAM_NAME:
|
||||
{
|
||||
if (c=='=')
|
||||
{
|
||||
buffer.setLength(nws_length); // trim following OWS
|
||||
if (nws_length-last_length==1 && Character.toLowerCase(buffer.charAt(last_length))=='q')
|
||||
{
|
||||
buffer.setLength(last_length-1);
|
||||
nws_length=buffer.length();
|
||||
last_length=nws_length;
|
||||
state=State.Q_VALUE;
|
||||
continue;
|
||||
}
|
||||
buffer.append(c);
|
||||
last_length=++nws_length;
|
||||
state=State.PARAM_VALUE;
|
||||
continue;
|
||||
}
|
||||
buffer.append(c);
|
||||
nws_length=buffer.length();
|
||||
continue;
|
||||
}
|
||||
|
||||
case PARAM_VALUE:
|
||||
case Q_VALUE:
|
||||
{
|
||||
buffer.append(c);
|
||||
nws_length=buffer.length();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
q=ZERO;
|
||||
}
|
||||
buffer.setLength(paramName-1);
|
||||
|
||||
while(_quality.size()<_values.size())
|
||||
_quality.add(ONE);
|
||||
_quality.add(q);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -246,7 +130,13 @@ public class QuotedQualityCSV implements Iterable<String>
|
|||
|
||||
last=q;
|
||||
len=v.length();
|
||||
|
||||
}
|
||||
|
||||
int last_element=_quality.size();
|
||||
while(last_element>0 && _quality.get(--last_element).equals(ZERO))
|
||||
{
|
||||
_quality.remove(last_element);
|
||||
_values.remove(last_element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,10 +31,11 @@ public class QuotedCSVTest
|
|||
public void testOWS()
|
||||
{
|
||||
QuotedCSV values = new QuotedCSV();
|
||||
values.addValue(" value 0.5 ; p = v ; q =0.5 , value 1.0 ");
|
||||
values.addValue(" value 0.5 ; pqy = vwz ; q =0.5 , value 1.0 , other ; param ");
|
||||
Assert.assertThat(values,Matchers.contains(
|
||||
"value 0.5;p=v;q=0.5",
|
||||
"value 1.0"));
|
||||
"value 0.5;pqy=vwz;q=0.5",
|
||||
"value 1.0",
|
||||
"other;param"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -88,6 +89,57 @@ public class QuotedCSVTest
|
|||
"value;p=v"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParamsOnly()
|
||||
{
|
||||
QuotedCSV values = new QuotedCSV(false);
|
||||
values.addValue("for=192.0.2.43, for=\"[2001:db8:cafe::17]\", for=unknown");
|
||||
assertThat(values,Matchers.contains(
|
||||
"for=192.0.2.43",
|
||||
"for=[2001:db8:cafe::17]",
|
||||
"for=unknown"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMutation()
|
||||
{
|
||||
QuotedCSV values = new QuotedCSV(false)
|
||||
{
|
||||
|
||||
@Override
|
||||
protected void parsedValue(StringBuffer buffer)
|
||||
{
|
||||
if (buffer.toString().contains("DELETE"))
|
||||
{
|
||||
String s = buffer.toString().replace("DELETE","");
|
||||
buffer.setLength(0);
|
||||
buffer.append(s);
|
||||
}
|
||||
if (buffer.toString().contains("APPEND"))
|
||||
{
|
||||
String s = buffer.toString().replace("APPEND","Append")+"!";
|
||||
buffer.setLength(0);
|
||||
buffer.append(s);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
|
||||
{
|
||||
String name = paramValue>0?buffer.substring(paramName,paramValue-1):buffer.substring(paramName);
|
||||
if ("IGNORE".equals(name))
|
||||
buffer.setLength(paramName-1);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
values.addValue("normal;param=val, testAPPENDandDELETEvalue ; n=v; IGNORE = this; x=y ");
|
||||
assertThat(values,Matchers.contains(
|
||||
"normal;param=val",
|
||||
"testAppendandvalue!;n=v;x=y"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUnQuote()
|
||||
{
|
||||
|
|
|
@ -74,6 +74,7 @@ public class QuotedQualityCSVTest
|
|||
values.addValue("*");
|
||||
values.addValue("compress;q=0.5, gzip;q=1.0");
|
||||
values.addValue("gzip;q=1.0, identity; q=0.5, *;q=0");
|
||||
|
||||
Assert.assertThat(values,Matchers.contains(
|
||||
"compress",
|
||||
"gzip",
|
||||
|
|
|
@ -23,22 +23,27 @@ import java.net.InetSocketAddress;
|
|||
import javax.servlet.ServletRequest;
|
||||
|
||||
import org.eclipse.jetty.http.HostPortHttpField;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpScheme;
|
||||
import org.eclipse.jetty.http.QuotedCSV;
|
||||
import org.eclipse.jetty.server.HttpConfiguration.Customizer;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Customize Requests for Proxy Forwarding.
|
||||
* <p>
|
||||
* This customizer looks at at HTTP request for headers that indicate
|
||||
* it has been forwarded by one or more proxies. Specifically handled are:
|
||||
* it has been forwarded by one or more proxies. Specifically handled are
|
||||
* <ul>
|
||||
* <li>Forwarded, as defined by <a href="https://tools.ietf.org/html/rfc7239">rfc7239</a>
|
||||
* <li>X-Forwarded-Host</li>
|
||||
* <li>X-Forwarded-Server</li>
|
||||
* <li>X-Forwarded-For</li>
|
||||
* <li>X-Forwarded-Proto</li>
|
||||
* <li>X-Proxied-Https</li>
|
||||
* </ul>
|
||||
* <p>If these headers are present, then the {@link Request} object is updated
|
||||
* so that the proxy is not seen as the other end point of the connection on which
|
||||
|
@ -49,44 +54,86 @@ import org.eclipse.jetty.server.HttpConfiguration.Customizer;
|
|||
*/
|
||||
public class ForwardedRequestCustomizer implements Customizer
|
||||
{
|
||||
private HostPortHttpField _hostHeader;
|
||||
|
||||
private HostPortHttpField _forcedHost;
|
||||
private String _forwardedHeader = HttpHeader.FORWARDED.toString();
|
||||
private String _forwardedHostHeader = HttpHeader.X_FORWARDED_HOST.toString();
|
||||
private String _forwardedServerHeader = HttpHeader.X_FORWARDED_SERVER.toString();
|
||||
private String _forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString();
|
||||
private String _forwardedProtoHeader = HttpHeader.X_FORWARDED_PROTO.toString();
|
||||
private String _forwardedHttpsHeader = "X-Proxied-Https";
|
||||
private String _forwardedCipherSuiteHeader;
|
||||
private String _forwardedSslSessionIdHeader;
|
||||
private boolean _proxyAsAuthority=false;
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public String getHostHeader()
|
||||
/**
|
||||
* @return true if the proxy address obtained via
|
||||
* X-Forwarded-Server or RFC7239 "by" is used as
|
||||
* the request authority. Default false
|
||||
*/
|
||||
public boolean getProxyAsAuthority()
|
||||
{
|
||||
return _hostHeader.getValue();
|
||||
return _proxyAsAuthority;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* @param proxyAsAuthority if true, use the proxy address obtained via
|
||||
* X-Forwarded-Server or RFC7239 "by" as the request authority.
|
||||
*/
|
||||
public void setProxyAsAuthority(boolean proxyAsAuthority)
|
||||
{
|
||||
_proxyAsAuthority = proxyAsAuthority;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configure to only support the RFC7239 Forwarded header and to
|
||||
* not support any X-Forwarded- headers.
|
||||
*/
|
||||
public void setForwardedOnly(boolean rfc7239only)
|
||||
{
|
||||
if (rfc7239only)
|
||||
{
|
||||
_forwardedHostHeader=null;
|
||||
_forwardedServerHeader=null;
|
||||
_forwardedForHeader=null;
|
||||
_forwardedProtoHeader=null;
|
||||
_forwardedHttpsHeader=null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_forwardedHostHeader = HttpHeader.X_FORWARDED_HOST.toString();
|
||||
_forwardedServerHeader = HttpHeader.X_FORWARDED_SERVER.toString();
|
||||
_forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString();
|
||||
_forwardedProtoHeader = HttpHeader.X_FORWARDED_PROTO.toString();
|
||||
_forwardedHttpsHeader = "X-Proxied-Https";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public String getForcedHost()
|
||||
{
|
||||
return _forcedHost.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a forced valued for the host header to control what is returned by {@link ServletRequest#getServerName()} and {@link ServletRequest#getServerPort()}.
|
||||
*
|
||||
* @param hostHeader
|
||||
* @param hostAndPort
|
||||
* The value of the host header to force.
|
||||
*/
|
||||
public void setHostHeader(String hostHeader)
|
||||
public void setForcedHost(String hostAndPort)
|
||||
{
|
||||
_hostHeader = new HostPortHttpField(hostHeader);
|
||||
_forcedHost = new HostPortHttpField(hostAndPort);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/*
|
||||
*
|
||||
* @see #setForwarded(boolean)
|
||||
*/
|
||||
public String getForwardedHostHeader()
|
||||
{
|
||||
return _forwardedHostHeader;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @param forwardedHostHeader
|
||||
* The header name for forwarded hosts (default x-forwarded-host)
|
||||
|
@ -96,7 +143,6 @@ public class ForwardedRequestCustomizer implements Customizer
|
|||
_forwardedHostHeader = forwardedHostHeader;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @return the header name for forwarded server.
|
||||
*/
|
||||
|
@ -105,7 +151,6 @@ public class ForwardedRequestCustomizer implements Customizer
|
|||
return _forwardedServerHeader;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @param forwardedServerHeader
|
||||
* The header name for forwarded server (default x-forwarded-server)
|
||||
|
@ -115,7 +160,6 @@ public class ForwardedRequestCustomizer implements Customizer
|
|||
_forwardedServerHeader = forwardedServerHeader;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @return the forwarded for header
|
||||
*/
|
||||
|
@ -124,7 +168,6 @@ public class ForwardedRequestCustomizer implements Customizer
|
|||
return _forwardedForHeader;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @param forwardedRemoteAddressHeader
|
||||
* The header name for forwarded for (default x-forwarded-for)
|
||||
|
@ -134,7 +177,6 @@ public class ForwardedRequestCustomizer implements Customizer
|
|||
_forwardedForHeader = forwardedRemoteAddressHeader;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* Get the forwardedProtoHeader.
|
||||
*
|
||||
|
@ -145,7 +187,6 @@ public class ForwardedRequestCustomizer implements Customizer
|
|||
return _forwardedProtoHeader;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* Set the forwardedProtoHeader.
|
||||
*
|
||||
|
@ -157,7 +198,6 @@ public class ForwardedRequestCustomizer implements Customizer
|
|||
_forwardedProtoHeader = forwardedProtoHeader;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @return The header name holding a forwarded cipher suite (default null)
|
||||
*/
|
||||
|
@ -166,7 +206,6 @@ public class ForwardedRequestCustomizer implements Customizer
|
|||
return _forwardedCipherSuiteHeader;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @param forwardedCipherSuite
|
||||
* The header name holding a forwarded cipher suite (default null)
|
||||
|
@ -176,7 +215,6 @@ public class ForwardedRequestCustomizer implements Customizer
|
|||
_forwardedCipherSuiteHeader = forwardedCipherSuite;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @return The header name holding a forwarded SSL Session ID (default null)
|
||||
*/
|
||||
|
@ -185,7 +223,6 @@ public class ForwardedRequestCustomizer implements Customizer
|
|||
return _forwardedSslSessionIdHeader;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @param forwardedSslSessionId
|
||||
* The header name holding a forwarded SSL Session ID (default null)
|
||||
|
@ -195,75 +232,135 @@ public class ForwardedRequestCustomizer implements Customizer
|
|||
_forwardedSslSessionIdHeader = forwardedSslSessionId;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @return The header name holding a forwarded Https (on|off true|false) value
|
||||
*/
|
||||
public String getForwardedHttpsHeader()
|
||||
{
|
||||
return _forwardedHttpsHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param forwardedHttpsHeader
|
||||
*/
|
||||
public void setForwardedHttpsHeader(String forwardedHttpsHeader)
|
||||
{
|
||||
_forwardedHttpsHeader = forwardedHttpsHeader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(Connector connector, HttpConfiguration config, Request request)
|
||||
{
|
||||
HttpFields httpFields = request.getHttpFields();
|
||||
|
||||
// Do SSL first
|
||||
if (getForwardedCipherSuiteHeader()!=null)
|
||||
RFC7239 rfc7230 = null;
|
||||
String forwardedHost = null;
|
||||
String forwardedServer = null;
|
||||
String forwardedFor = null;
|
||||
String forwardedProto = null;
|
||||
String forwardedHttps = null;
|
||||
|
||||
// Do a single pass through the header fields as it is a more efficient single iteration.
|
||||
for (HttpField field : httpFields)
|
||||
{
|
||||
String cipher_suite=httpFields.get(getForwardedCipherSuiteHeader());
|
||||
if (cipher_suite!=null)
|
||||
request.setAttribute("javax.servlet.request.cipher_suite",cipher_suite);
|
||||
}
|
||||
if (getForwardedSslSessionIdHeader()!=null)
|
||||
{
|
||||
String ssl_session_id=httpFields.get(getForwardedSslSessionIdHeader());
|
||||
if(ssl_session_id!=null)
|
||||
String name = field.getName();
|
||||
|
||||
if (getForwardedCipherSuiteHeader()!=null && httpFields.get(getForwardedCipherSuiteHeader()).equalsIgnoreCase(name))
|
||||
request.setAttribute("javax.servlet.request.cipher_suite",field.getValue());
|
||||
|
||||
if (getForwardedSslSessionIdHeader()!=null && httpFields.get(getForwardedSslSessionIdHeader()).equalsIgnoreCase(name))
|
||||
request.setAttribute("javax.servlet.request.ssl_session_id", field.getValue());
|
||||
|
||||
if (forwardedHost==null && _forwardedHostHeader!=null && _forwardedHostHeader.equalsIgnoreCase(name))
|
||||
forwardedHost = getLeftMost(field.getValue());
|
||||
|
||||
if (forwardedServer==null && _forwardedServerHeader!=null && _forwardedServerHeader.equalsIgnoreCase(name))
|
||||
forwardedServer = getLeftMost(field.getValue());
|
||||
|
||||
if (forwardedFor==null && _forwardedForHeader!=null && _forwardedForHeader.equalsIgnoreCase(name))
|
||||
forwardedFor = getLeftMost(field.getValue());
|
||||
|
||||
if (forwardedProto==null && _forwardedProtoHeader!=null && _forwardedProtoHeader.equalsIgnoreCase(name))
|
||||
forwardedProto = getLeftMost(field.getValue());
|
||||
|
||||
if (forwardedHttps==null && _forwardedHttpsHeader!=null && _forwardedHttpsHeader.equalsIgnoreCase(name))
|
||||
forwardedHttps = getLeftMost(field.getValue());
|
||||
|
||||
if (_forwardedHeader!=null && _forwardedHeader.equalsIgnoreCase(name))
|
||||
{
|
||||
request.setAttribute("javax.servlet.request.ssl_session_id", ssl_session_id);
|
||||
request.setScheme(HttpScheme.HTTPS.asString());
|
||||
if (rfc7230==null)
|
||||
rfc7230= new RFC7239();
|
||||
rfc7230.addValue(field.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieving headers from the request
|
||||
String forwardedHost = getLeftMostFieldValue(httpFields,getForwardedHostHeader());
|
||||
String forwardedServer = getLeftMostFieldValue(httpFields,getForwardedServerHeader());
|
||||
String forwardedFor = getLeftMostFieldValue(httpFields,getForwardedForHeader());
|
||||
String forwardedProto = getLeftMostFieldValue(httpFields,getForwardedProtoHeader());
|
||||
|
||||
if (_hostHeader != null)
|
||||
|
||||
// Handle host header if if not available any RFC7230.by or X-ForwardedServer header
|
||||
if (_forcedHost != null)
|
||||
{
|
||||
// Update host header
|
||||
httpFields.put(_hostHeader);
|
||||
request.setAuthority(_hostHeader.getHost(),_hostHeader.getPort());
|
||||
httpFields.put(_forcedHost);
|
||||
request.setAuthority(_forcedHost.getHost(),_forcedHost.getPort());
|
||||
}
|
||||
else if (rfc7230!=null && rfc7230._host!=null)
|
||||
{
|
||||
HostPortHttpField auth = rfc7230._host;
|
||||
httpFields.put(auth);
|
||||
request.setAuthority(auth.getHost(),auth.getPort());
|
||||
}
|
||||
else if (forwardedHost != null)
|
||||
{
|
||||
// Update host header
|
||||
HostPortHttpField auth = new HostPortHttpField(forwardedHost);
|
||||
httpFields.put(auth);
|
||||
request.setAuthority(auth.getHost(),auth.getPort());
|
||||
}
|
||||
else if (forwardedServer != null)
|
||||
else if (_proxyAsAuthority)
|
||||
{
|
||||
// Use provided server name
|
||||
request.setAuthority(forwardedServer,request.getServerPort());
|
||||
if (rfc7230!=null && rfc7230._by!=null)
|
||||
{
|
||||
HostPortHttpField auth = rfc7230._by;
|
||||
httpFields.put(auth);
|
||||
request.setAuthority(auth.getHost(),auth.getPort());
|
||||
}
|
||||
else if (forwardedServer != null)
|
||||
{
|
||||
request.setAuthority(forwardedServer,request.getServerPort());
|
||||
}
|
||||
}
|
||||
|
||||
if (forwardedFor != null)
|
||||
// handle remote end identifier
|
||||
if (rfc7230!=null && rfc7230._for!=null)
|
||||
{
|
||||
request.setRemoteAddr(InetSocketAddress.createUnresolved(rfc7230._for.getHost(),rfc7230._for.getPort()));
|
||||
}
|
||||
else if (forwardedFor != null)
|
||||
{
|
||||
request.setRemoteAddr(InetSocketAddress.createUnresolved(forwardedFor,request.getRemotePort()));
|
||||
}
|
||||
|
||||
if (forwardedProto != null)
|
||||
// handle protocol identifier
|
||||
if (rfc7230!=null && rfc7230._proto!=null)
|
||||
{
|
||||
request.setScheme(rfc7230._proto);
|
||||
if (rfc7230._proto.equals(config.getSecureScheme()))
|
||||
request.setSecure(true);
|
||||
}
|
||||
else if (forwardedProto != null)
|
||||
{
|
||||
request.setScheme(forwardedProto);
|
||||
if (forwardedProto.equals(config.getSecureScheme()))
|
||||
request.setSecure(true);
|
||||
}
|
||||
else if (forwardedHttps !=null && ("on".equalsIgnoreCase(forwardedHttps)||"true".equalsIgnoreCase(forwardedHttps)))
|
||||
{
|
||||
request.setScheme(HttpScheme.HTTPS.asString());
|
||||
if (HttpScheme.HTTPS.asString().equals(config.getSecureScheme()))
|
||||
request.setSecure(true);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
protected String getLeftMostFieldValue(HttpFields fields, String header)
|
||||
protected String getLeftMost(String headerValue)
|
||||
{
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
String headerValue = fields.get(header);
|
||||
|
||||
if (headerValue == null)
|
||||
return null;
|
||||
|
||||
|
@ -276,14 +373,76 @@ public class ForwardedRequestCustomizer implements Customizer
|
|||
}
|
||||
|
||||
// The left-most value is the farthest downstream client
|
||||
return headerValue.substring(0,commaIndex);
|
||||
return headerValue.substring(0,commaIndex).trim();
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s@%x",this.getClass().getSimpleName(),hashCode());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public String getHostHeader()
|
||||
{
|
||||
return _forcedHost.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a forced valued for the host header to control what is returned by {@link ServletRequest#getServerName()} and {@link ServletRequest#getServerPort()}.
|
||||
*
|
||||
* @param hostHeader
|
||||
* The value of the host header to force.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setHostHeader(String hostHeader)
|
||||
{
|
||||
_forcedHost = new HostPortHttpField(hostHeader);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* ------------------------------------------------------------ */
|
||||
/* ------------------------------------------------------------ */
|
||||
private final class RFC7239 extends QuotedCSV
|
||||
{
|
||||
HostPortHttpField _by;
|
||||
HostPortHttpField _for;
|
||||
HostPortHttpField _host;
|
||||
String _proto;
|
||||
|
||||
private RFC7239()
|
||||
{
|
||||
super(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
|
||||
{
|
||||
if (valueLength==0 && paramValue>paramName)
|
||||
{
|
||||
String name=StringUtil.asciiToLowerCase(buffer.substring(paramName,paramValue-1));
|
||||
String value=buffer.substring(paramValue);
|
||||
switch(name)
|
||||
{
|
||||
case "by":
|
||||
if (_by==null && !value.startsWith("_") && !"unknown".equals(value))
|
||||
_by=new HostPortHttpField(value);
|
||||
break;
|
||||
case "for":
|
||||
if (_for==null && !value.startsWith("_") && !"unknown".equals(value))
|
||||
_for=new HostPortHttpField(value);
|
||||
break;
|
||||
case "host":
|
||||
if (_host==null)
|
||||
_host=new HostPortHttpField(value);
|
||||
break;
|
||||
case "proto":
|
||||
if (_proto==null)
|
||||
_proto=value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,235 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.server;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ForwardedRequestCustomizerTest
|
||||
{
|
||||
private Server _server;
|
||||
private LocalConnector _connector;
|
||||
private RequestHandler _handler;
|
||||
final Deque<String> _results = new ArrayDeque<>();
|
||||
|
||||
@Before
|
||||
public void init() throws Exception
|
||||
{
|
||||
_server = new Server();
|
||||
HttpConnectionFactory http = new HttpConnectionFactory();
|
||||
http.setInputBufferSize(1024);
|
||||
http.getHttpConfiguration().setRequestHeaderSize(512);
|
||||
http.getHttpConfiguration().setResponseHeaderSize(512);
|
||||
http.getHttpConfiguration().setOutputBufferSize(2048);
|
||||
http.getHttpConfiguration().addCustomizer(new ForwardedRequestCustomizer());
|
||||
_connector = new LocalConnector(_server,http);
|
||||
_server.addConnector(_connector);
|
||||
_handler = new RequestHandler();
|
||||
_server.setHandler(_handler);
|
||||
|
||||
_handler._checker = new RequestTester()
|
||||
{
|
||||
@Override
|
||||
public boolean check(HttpServletRequest request,HttpServletResponse response)
|
||||
{
|
||||
_results.add(request.getScheme());
|
||||
_results.add(request.getServerName());
|
||||
_results.add(Integer.toString(request.getServerPort()));
|
||||
_results.add(request.getRemoteAddr());
|
||||
_results.add(Integer.toString(request.getRemotePort()));
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
_server.start();
|
||||
}
|
||||
|
||||
@After
|
||||
public void destroy() throws Exception
|
||||
{
|
||||
_server.stop();
|
||||
_server.join();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testRFC7239_Examples_4() throws Exception
|
||||
{
|
||||
String response=_connector.getResponse(
|
||||
"GET / HTTP/1.1\n"+
|
||||
"Host: myhost\n"+
|
||||
"Forwarded: for=\"_gazonk\"\n"+
|
||||
"Forwarded: For=\"[2001:db8:cafe::17]:4711\"\n"+
|
||||
"Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43\n"+
|
||||
"Forwarded: for=192.0.2.43, for=198.51.100.17\n"+
|
||||
"\n");
|
||||
assertThat(response, Matchers.containsString("200 OK"));
|
||||
assertEquals("http",_results.poll());
|
||||
assertEquals("myhost",_results.poll());
|
||||
assertEquals("80",_results.poll());
|
||||
assertEquals("[2001:db8:cafe::17]",_results.poll());
|
||||
assertEquals("4711",_results.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRFC7239_Examples_7_1() throws Exception
|
||||
{
|
||||
_connector.getResponse(
|
||||
"GET / HTTP/1.1\n"+
|
||||
"Host: myhost\n"+
|
||||
"Forwarded: for=192.0.2.43,for=\"[2001:db8:cafe::17]\",for=unknown\n"+
|
||||
"\n");
|
||||
_connector.getResponse(
|
||||
"GET / HTTP/1.1\n"+
|
||||
"Host: myhost\n"+
|
||||
"Forwarded: for=192.0.2.43, for=\"[2001:db8:cafe::17]\", for=unknown\n"+
|
||||
"\n");
|
||||
_connector.getResponse(
|
||||
"GET / HTTP/1.1\n"+
|
||||
"Host: myhost\n"+
|
||||
"Forwarded: for=192.0.2.43\n"+
|
||||
"Forwarded: for=\"[2001:db8:cafe::17]\", for=unknown\n"+
|
||||
"\n");
|
||||
|
||||
assertEquals("http",_results.poll());
|
||||
assertEquals("myhost",_results.poll());
|
||||
assertEquals("80",_results.poll());
|
||||
assertEquals("192.0.2.43",_results.poll());
|
||||
assertEquals("0",_results.poll());
|
||||
assertEquals("http",_results.poll());
|
||||
assertEquals("myhost",_results.poll());
|
||||
assertEquals("80",_results.poll());
|
||||
assertEquals("192.0.2.43",_results.poll());
|
||||
assertEquals("0",_results.poll());
|
||||
assertEquals("http",_results.poll());
|
||||
assertEquals("myhost",_results.poll());
|
||||
assertEquals("80",_results.poll());
|
||||
assertEquals("192.0.2.43",_results.poll());
|
||||
assertEquals("0",_results.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRFC7239_Examples_7_4() throws Exception
|
||||
{
|
||||
_connector.getResponse(
|
||||
"GET / HTTP/1.1\n"+
|
||||
"Host: myhost\n"+
|
||||
"Forwarded: for=192.0.2.43, for=\"[2001:db8:cafe::17]\"\n"+
|
||||
"\n");
|
||||
|
||||
assertEquals("http",_results.poll());
|
||||
assertEquals("myhost",_results.poll());
|
||||
assertEquals("80",_results.poll());
|
||||
assertEquals("192.0.2.43",_results.poll());
|
||||
assertEquals("0",_results.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRFC7239_Examples_7_5() throws Exception
|
||||
{
|
||||
_connector.getResponse(
|
||||
"GET / HTTP/1.1\n"+
|
||||
"Host: myhost\n"+
|
||||
"Forwarded: for=192.0.2.43,for=198.51.100.17;by=203.0.113.60;proto=http;host=example.com\n"+
|
||||
"\n");
|
||||
|
||||
assertEquals("http",_results.poll());
|
||||
assertEquals("example.com",_results.poll());
|
||||
assertEquals("80",_results.poll());
|
||||
assertEquals("192.0.2.43",_results.poll());
|
||||
assertEquals("0",_results.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProto() throws Exception
|
||||
{
|
||||
String response=_connector.getResponse(
|
||||
"GET / HTTP/1.1\n"+
|
||||
"Host: myhost\n"+
|
||||
"X-Forwarded-Proto: foobar\n"+
|
||||
"Forwarded: proto=https\n"+
|
||||
"\n");
|
||||
assertThat(response, Matchers.containsString("200 OK"));
|
||||
assertEquals("https",_results.poll());
|
||||
assertEquals("myhost",_results.poll());
|
||||
assertEquals("443",_results.poll());
|
||||
assertEquals("0.0.0.0",_results.poll());
|
||||
assertEquals("0",_results.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLegacyProto() throws Exception
|
||||
{
|
||||
String response=_connector.getResponse(
|
||||
"GET / HTTP/1.1\n"+
|
||||
"Host: myhost\n"+
|
||||
"X-Proxied-Https: on\n"+
|
||||
"\n");
|
||||
assertThat(response, Matchers.containsString("200 OK"));
|
||||
assertEquals("https",_results.poll());
|
||||
assertEquals("myhost",_results.poll());
|
||||
assertEquals("443",_results.poll());
|
||||
assertEquals("0.0.0.0",_results.poll());
|
||||
assertEquals("0",_results.poll());
|
||||
}
|
||||
|
||||
interface RequestTester
|
||||
{
|
||||
boolean check(HttpServletRequest request,HttpServletResponse response) throws IOException;
|
||||
}
|
||||
|
||||
private class RequestHandler extends AbstractHandler
|
||||
{
|
||||
private RequestTester _checker;
|
||||
@SuppressWarnings("unused")
|
||||
private String _content;
|
||||
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
((Request)request).setHandled(true);
|
||||
|
||||
if (request.getContentLength()>0
|
||||
&& !MimeTypes.Type.FORM_ENCODED.asString().equals(request.getContentType())
|
||||
&& !request.getContentType().startsWith("multipart/form-data"))
|
||||
_content=IO.toString(request.getInputStream());
|
||||
|
||||
if (_checker!=null && _checker.check(request,response))
|
||||
response.setStatus(200);
|
||||
else
|
||||
response.sendError(500);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue