diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java index 98516f73faf..6de3357e44a 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java @@ -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 (posThis factory can be placed in front of any other connection factory + * to process the proxy line before the normal protocol handling

+ * + * @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(); + } + } +} diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java b/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java index 36e91052fd3..edc45fc6989 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java @@ -103,6 +103,8 @@ public class DumpHandler extends AbstractHandler writer.write("
\ncontentType="+request.getContentType()+"\n
\n"); writer.write("
\nencoding="+request.getCharacterEncoding()+"\n
\n"); writer.write("
\nservername="+request.getServerName()+"\n
\n"); + writer.write("
\nlocal="+request.getLocalAddr()+":"+request.getLocalPort()+"\n
\n"); + writer.write("
\nremote="+request.getRemoteAddr()+":"+request.getRemotePort()+"\n
\n"); writer.write("

Header:

");
         writer.write(request.getMethod()+" "+request.getRequestURI()+" "+request.getProtocol()+"\n");
         Enumeration headers = request.getHeaderNames();
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java
new file mode 100644
index 00000000000..1593372fdbd
--- /dev/null
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java
@@ -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(""));
+    }
+}
+
+
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
index 5af0db73a0c..d1a358a0283 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
@@ -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)