Implemented the PROXY protocol

Moved the PROXY protocol support from HttpParser to a ConnectionFactory.
See http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt
This commit is contained in:
Greg Wilkins 2014-11-13 19:55:01 +11:00
parent 59fc60972e
commit a308c087ed
7 changed files with 507 additions and 52 deletions

View File

@ -632,28 +632,8 @@ public class HttpParser
version=HttpVersion.lookAheadGet(buffer.array(),buffer.arrayOffset()+buffer.position()-1,buffer.arrayOffset()+buffer.limit());
else
version=HttpVersion.CACHE.getBest(buffer,0,buffer.remaining());
if (version==null)
{
if (_method==HttpMethod.PROXY)
{
if (!(_requestHandler instanceof ProxyHandler))
throw new BadMessageException();
String protocol=_uri.toString();
// This is the proxy protocol, so we can assume entire first line is in buffer else 400
buffer.position(buffer.position()-1);
String sAddr = getProxyField(buffer);
String dAddr = getProxyField(buffer);
int sPort = BufferUtil.takeInt(buffer);
next(buffer);
int dPort = BufferUtil.takeInt(buffer);
next(buffer);
_state=State.START;
((ProxyHandler)_requestHandler).proxied(protocol,sAddr,dAddr,sPort,dPort);
return false;
}
}
else
if (version!=null)
{
int pos = buffer.position()+version.asString().length()-1;
if (pos<buffer.limit())
@ -1574,14 +1554,6 @@ public class HttpParser
public int getHeaderCacheSize();
}
/* ------------------------------------------------------------------------------- */
/* ------------------------------------------------------------------------------- */
/* ------------------------------------------------------------------------------- */
public interface ProxyHandler
{
void proxied(String protocol, String sAddr, String dAddr, int sPort, int dPort);
}
/* ------------------------------------------------------------------------------- */
/* ------------------------------------------------------------------------------- */
/* ------------------------------------------------------------------------------- */

View File

@ -1633,7 +1633,7 @@ public class HttpParserTest
private boolean _headerCompleted;
private boolean _messageCompleted;
private class Handler implements HttpParser.RequestHandler, HttpParser.ResponseHandler, HttpParser.ProxyHandler
private class Handler implements HttpParser.RequestHandler, HttpParser.ResponseHandler
{
private HttpFields fields;
String _proxy;
@ -1742,11 +1742,5 @@ public class HttpParserTest
{
return 512;
}
@Override
public void proxied(String protocol, String sAddr, String dAddr, int sPort, int dPort)
{
_proxy="PROXY "+protocol+" "+sAddr+" "+dAddr+" "+sPort+" "+dPort;
}
}
}

View File

@ -42,7 +42,7 @@ import org.eclipse.jetty.util.log.Logger;
/**
* A HttpChannel customized to be transported over the HTTP/1 protocol
*/
class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandler, HttpParser.ProxyHandler
class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandler
{
private static final Logger LOG = Log.getLogger(HttpChannelOverHttp.class);
@ -99,16 +99,6 @@ class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandl
_expect102Processing = false;
return false;
}
@Override
public void proxied(String protocol, String sAddr, String dAddr, int sPort, int dPort)
{
_metadata.setMethod(HttpMethod.CONNECT.asString());
Request request = getRequest();
request.setAttribute("PROXY", protocol);
request.setAuthority(sAddr,dPort);
request.setRemoteAddr(InetSocketAddress.createUnresolved(sAddr,sPort));
}
@Override
public void parsedHeader(HttpField field)

View File

@ -0,0 +1,306 @@
//
// ========================================================================
// Copyright (c) 1995-2014 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 java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ReadPendingException;
import java.nio.channels.WritePendingException;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/* ------------------------------------------------------------ */
/** ConnectionFactory for the PROXY Protocol.
* <p>This factory can be placed in front of any other connection factory
* to process the proxy line before the normal protocol handling</p>
*
* @see http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt
*/
public class ProxyConnectionFactory extends AbstractConnectionFactory
{
private static final Logger LOG = Log.getLogger(ProxyConnectionFactory.class);
private final String _next;
public ProxyConnectionFactory(String nextProtocol)
{
super("haproxy");
_next=nextProtocol;
}
@Override
public Connection newConnection(Connector connector, EndPoint endp)
{
return new ProxyConnection(endp,connector,_next);
}
public static class ProxyConnection extends AbstractConnection
{
// 0 1 2 3 4 5 6
// 98765432109876543210987654321
// PROXY P R.R.R.R L.L.L.L R Lrn
private final int[] __size = {29,23,21,13,5,3,1};
private final Connector _connector;
private final String _next;
private final StringBuilder _builder=new StringBuilder();
private final String[] _field=new String[6];
private int _fields;
private int _length;
protected ProxyConnection(EndPoint endp, Connector connector, String next)
{
super(endp,connector.getExecutor(),false);
_connector=connector;
_next=next;
}
@Override
public void onOpen()
{
super.onOpen();
fillInterested();
}
@Override
public void onFillable()
{
try
{
ByteBuffer buffer=null;
loop: while(true)
{
// Create a buffer that will not read too much data
int size=Math.max(1,__size[_fields]-_builder.length());
if (buffer==null || buffer.capacity()!=size)
buffer=BufferUtil.allocate(size);
else
BufferUtil.clear(buffer);
// Read data
int fill=getEndPoint().fill(buffer);
if (fill<0)
{
getEndPoint().shutdownOutput();
return;
}
if (fill==0)
{
fillInterested();
return;
}
_length+=fill;
if (_length>=108)
{
LOG.warn("PROXY line too long {}",getEndPoint());
close();
return;
}
// parse fields
while (buffer.hasRemaining())
{
byte b = buffer.get();
if (_fields<6)
{
if (b==' ' || b=='\r' && _fields==5)
{
_field[_fields++]=_builder.toString();
_builder.setLength(0);
}
else if (b<' ')
{
LOG.warn("Bad char {}",getEndPoint());
close();
return;
}
else
_builder.append((char)b);
}
else
{
if (b=='\n')
break loop;
LOG.warn("Bad CRLF {}",getEndPoint());
close();
return;
}
}
}
// Check proxy
if (!"PROXY".equals(_field[0]))
{
LOG.warn("Bad PROXY {}",getEndPoint());
close();
return;
}
// Extract Addresses
InetSocketAddress remote=new InetSocketAddress(_field[2],Integer.parseInt(_field[4]));
InetSocketAddress local =new InetSocketAddress(_field[3],Integer.parseInt(_field[5]));
// Create the next protocol
ConnectionFactory connectionFactory = _connector.getConnectionFactory(_next);
if (connectionFactory == null)
{
LOG.info("{} next protocol '{}'",getEndPoint(), _next);
close();
return;
}
EndPoint endPoint = new ProxyEndPoint(getEndPoint(),remote,local);
Connection oldConnection = getEndPoint().getConnection();
Connection newConnection = connectionFactory.newConnection(_connector, endPoint);
if (LOG.isDebugEnabled())
LOG.debug("Switching to {} {}", _next, getEndPoint());
oldConnection.onClose();
endPoint.setConnection(newConnection);
newConnection.onOpen();
}
catch (Throwable e)
{
LOG.warn("Bad PROXY {} {}",e.toString(),getEndPoint());
LOG.debug(e);
close();
}
}
}
public static class ProxyEndPoint implements EndPoint
{
private final EndPoint _endp;
private final InetSocketAddress _remote;
private final InetSocketAddress _local;
public ProxyEndPoint(EndPoint endp, InetSocketAddress remote, InetSocketAddress local)
{
_endp=endp;
_remote=remote;
_local=local;
}
public InetSocketAddress getLocalAddress()
{
return _local;
}
public InetSocketAddress getRemoteAddress()
{
return _remote;
}
public boolean isOpen()
{
return _endp.isOpen();
}
public long getCreatedTimeStamp()
{
return _endp.getCreatedTimeStamp();
}
public void shutdownOutput()
{
_endp.shutdownOutput();
}
public boolean isOutputShutdown()
{
return _endp.isOutputShutdown();
}
public boolean isInputShutdown()
{
return _endp.isInputShutdown();
}
public void close()
{
_endp.close();
}
public int fill(ByteBuffer buffer) throws IOException
{
return _endp.fill(buffer);
}
public boolean flush(ByteBuffer... buffer) throws IOException
{
return _endp.flush(buffer);
}
public Object getTransport()
{
return _endp.getTransport();
}
public long getIdleTimeout()
{
return _endp.getIdleTimeout();
}
public void setIdleTimeout(long idleTimeout)
{
_endp.setIdleTimeout(idleTimeout);
}
public void fillInterested(Callback callback) throws ReadPendingException
{
_endp.fillInterested(callback);
}
public void write(Callback callback, ByteBuffer... buffers) throws WritePendingException
{
_endp.write(callback,buffers);
}
public Connection getConnection()
{
return _endp.getConnection();
}
public void setConnection(Connection connection)
{
_endp.setConnection(connection);
}
public void onOpen()
{
_endp.onOpen();
}
public void onClose()
{
_endp.onClose();
}
}
}

View File

@ -103,6 +103,8 @@ public class DumpHandler extends AbstractHandler
writer.write("<pre>\ncontentType="+request.getContentType()+"\n</pre>\n");
writer.write("<pre>\nencoding="+request.getCharacterEncoding()+"\n</pre>\n");
writer.write("<pre>\nservername="+request.getServerName()+"\n</pre>\n");
writer.write("<pre>\nlocal="+request.getLocalAddr()+":"+request.getLocalPort()+"\n</pre>\n");
writer.write("<pre>\nremote="+request.getRemoteAddr()+":"+request.getRemotePort()+"\n</pre>\n");
writer.write("<h3>Header:</h3><pre>");
writer.write(request.getMethod()+" "+request.getRequestURI()+" "+request.getProtocol()+"\n");
Enumeration<String> headers = request.getHeaderNames();

View File

@ -0,0 +1,171 @@
//
// ========================================================================
// Copyright (c) 1995-2014 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 org.eclipse.jetty.server.handler.ErrorHandler;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
*
*/
public class ProxyConnectionTest
{
private Server _server;
private LocalConnector _connector;
@Before
public void init() throws Exception
{
_server = new Server();
HttpConnectionFactory http = new HttpConnectionFactory();
http.getHttpConfiguration().setRequestHeaderSize(1024);
http.getHttpConfiguration().setResponseHeaderSize(1024);
ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol());
_connector = new LocalConnector(_server,null,null,null,1,proxy,http);
_connector.setIdleTimeout(1000);
_server.addConnector(_connector);
_server.setHandler(new DumpHandler());
ErrorHandler eh=new ErrorHandler();
eh.setServer(_server);
_server.addBean(eh);
_server.start();
}
@After
public void destroy() throws Exception
{
_server.stop();
_server.join();
}
@Test
public void testSimple() throws Exception
{
String response=_connector.getResponses("PROXY TCP 1.2.3.4 5.6.7.8 111 222\r\n"+
"GET /path HTTP/1.1\n"+
"Host: server:80\n"+
"Connection: close\n"+
"\n");
Assert.assertThat(response,Matchers.containsString("HTTP/1.1 200"));
Assert.assertThat(response,Matchers.containsString("pathInfo=/path"));
Assert.assertThat(response,Matchers.containsString("local=5.6.7.8:222"));
Assert.assertThat(response,Matchers.containsString("remote=1.2.3.4:111"));
}
@Test
public void testIPv6() throws Exception
{
String response=_connector.getResponses("PROXY UNKNOWN eeee:eeee:eeee:eeee:eeee:eeee:eeee:eeee ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 65535 65535\r\n"+
"GET /path HTTP/1.1\n"+
"Host: server:80\n"+
"Connection: close\n"+
"\n");
Assert.assertThat(response,Matchers.containsString("HTTP/1.1 200"));
Assert.assertThat(response,Matchers.containsString("pathInfo=/path"));
Assert.assertThat(response,Matchers.containsString("remote=eeee:eeee:eeee:eeee:eeee:eeee:eeee:eeee:65535"));
Assert.assertThat(response,Matchers.containsString("local=ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:65535"));
}
@Test
public void testTooLong() throws Exception
{
String response=_connector.getResponses("PROXY TOOLONG!!! eeee:eeee:eeee:eeee:0000:0000:0000:0000 ffff:ffff:ffff:ffff:0000:0000:0000:0000 65535 65535\r\n"+
"GET /path HTTP/1.1\n"+
"Host: server:80\n"+
"Connection: close\n"+
"\n");
Assert.assertThat(response,Matchers.equalTo(""));
}
@Test
public void testNotComplete() throws Exception
{
_connector.setIdleTimeout(100);
String response=_connector.getResponses("PROXY TIMEOUT");
Assert.assertThat(response,Matchers.equalTo(""));
}
@Test
public void testBadChar() throws Exception
{
String response=_connector.getResponses("PROXY\tTCP 1.2.3.4 5.6.7.8 111 222\r\n"+
"GET /path HTTP/1.1\n"+
"Host: server:80\n"+
"Connection: close\n"+
"\n");
Assert.assertThat(response,Matchers.equalTo(""));
}
@Test
public void testBadCRLF() throws Exception
{
String response=_connector.getResponses("PROXY TCP 1.2.3.4 5.6.7.8 111 222\r \n"+
"GET /path HTTP/1.1\n"+
"Host: server:80\n"+
"Connection: close\n"+
"\n");
Assert.assertThat(response,Matchers.equalTo(""));
}
@Test
public void testBadPort() throws Exception
{
String response=_connector.getResponses("PROXY TCP 1.2.3.4 5.6.7.8 9999999999999 222\r\n"+
"GET /path HTTP/1.1\n"+
"Host: server:80\n"+
"Connection: close\n"+
"\n");
Assert.assertThat(response,Matchers.equalTo(""));
}
@Test
public void testMissingField() throws Exception
{
String response=_connector.getResponses("PROXY TCP 1.2.3.4 5.6.7.8 222\r\n"+
"GET /path HTTP/1.1\n"+
"Host: server:80\n"+
"Connection: close\n"+
"\n");
Assert.assertThat(response,Matchers.equalTo(""));
}
@Test
public void testHTTP() throws Exception
{
String response=_connector.getResponses(
"GET /path HTTP/1.1\n"+
"Host: server:80\n"+
"Connection: close\n"+
"\n");
Assert.assertThat(response,Matchers.equalTo(""));
}
}

View File

@ -516,10 +516,12 @@ public class BufferUtil
}
/* ------------------------------------------------------------ */
/** Convert a partial buffer to a String
* @param buffer The buffer to convert in flush mode. The buffer is unchanged
/** Convert a partial buffer to a String.
*
* @param position The position in the buffer to start the string from
* @param length The length of the buffer
* @param charset The {@link Charset} to use to convert the bytes
* @return The buffer as a string.
* @return The buffer as a string.
*/
public static String toString(ByteBuffer buffer, int position, int length, Charset charset)
{
@ -547,12 +549,30 @@ public class BufferUtil
* @return an int
*/
public static int toInt(ByteBuffer buffer)
{
return toInt(buffer,buffer.position(),buffer.remaining());
}
/* ------------------------------------------------------------ */
/**
* Convert buffer to an integer. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown
*
* @param buffer
* A buffer containing an integer in flush mode. The position is not changed.
* @return an int
*/
public static int toInt(ByteBuffer buffer, int position, int length)
{
int val = 0;
boolean started = false;
boolean minus = false;
for (int i = buffer.position(); i < buffer.limit(); i++)
int limit = position+length;
if (length<=0)
throw new NumberFormatException(toString(buffer,position,length,StandardCharsets.UTF_8));
for (int i = position; i < limit; i++)
{
byte b = buffer.get(i);
if (b <= SPACE)