| // |
| // ======================================================================== |
| // Copyright (c) 1995-2017 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.spdy.server.proxy; |
| |
| import java.nio.ByteBuffer; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.jetty.http.HttpField; |
| import org.eclipse.jetty.http.HttpFields; |
| import org.eclipse.jetty.http.HttpGenerator; |
| import org.eclipse.jetty.http.HttpHeader; |
| import org.eclipse.jetty.http.HttpHeaderValue; |
| import org.eclipse.jetty.http.HttpMethod; |
| import org.eclipse.jetty.http.HttpParser; |
| import org.eclipse.jetty.http.HttpVersion; |
| import org.eclipse.jetty.io.EndPoint; |
| import org.eclipse.jetty.server.Connector; |
| import org.eclipse.jetty.server.HttpConfiguration; |
| import org.eclipse.jetty.server.HttpConnection; |
| import org.eclipse.jetty.server.SslConnectionFactory; |
| import org.eclipse.jetty.spdy.ISession; |
| import org.eclipse.jetty.spdy.IStream; |
| import org.eclipse.jetty.spdy.StandardSession; |
| import org.eclipse.jetty.spdy.StandardStream; |
| import org.eclipse.jetty.spdy.api.ByteBufferDataInfo; |
| import org.eclipse.jetty.spdy.api.DataInfo; |
| import org.eclipse.jetty.spdy.api.GoAwayInfo; |
| import org.eclipse.jetty.spdy.api.GoAwayResultInfo; |
| import org.eclipse.jetty.spdy.api.HeadersInfo; |
| import org.eclipse.jetty.spdy.api.PushInfo; |
| import org.eclipse.jetty.spdy.api.ReplyInfo; |
| import org.eclipse.jetty.spdy.api.RstInfo; |
| import org.eclipse.jetty.spdy.api.SessionStatus; |
| import org.eclipse.jetty.spdy.api.Stream; |
| import org.eclipse.jetty.spdy.api.StreamFrameListener; |
| import org.eclipse.jetty.spdy.api.SynInfo; |
| import org.eclipse.jetty.spdy.http.HTTPSPDYHeader; |
| import org.eclipse.jetty.util.BufferUtil; |
| import org.eclipse.jetty.util.Callback; |
| import org.eclipse.jetty.util.Fields; |
| import org.eclipse.jetty.util.Promise; |
| |
| public class ProxyHTTPSPDYConnection extends HttpConnection implements HttpParser.RequestHandler<ByteBuffer> |
| { |
| private final short version; |
| private final Fields headers = new Fields(); |
| private final ProxyEngineSelector proxyEngineSelector; |
| private final ISession session; |
| private HTTPStream stream; |
| private ByteBuffer content; |
| |
| public ProxyHTTPSPDYConnection(Connector connector, HttpConfiguration config, EndPoint endPoint, short version, ProxyEngineSelector proxyEngineSelector) |
| { |
| super(config, connector, endPoint); |
| this.version = version; |
| this.proxyEngineSelector = proxyEngineSelector; |
| this.session = new HTTPSession(version, connector); |
| } |
| |
| @Override |
| protected HttpParser.RequestHandler<ByteBuffer> newRequestHandler() |
| { |
| return this; |
| } |
| |
| @Override |
| public boolean startRequest(HttpMethod method, String methodString, ByteBuffer uri, HttpVersion httpVersion) |
| { |
| Connector connector = getConnector(); |
| String scheme = connector.getConnectionFactory(SslConnectionFactory.class) != null ? "https" : "http"; |
| headers.put(HTTPSPDYHeader.SCHEME.name(version), scheme); |
| headers.put(HTTPSPDYHeader.METHOD.name(version), methodString); |
| headers.put(HTTPSPDYHeader.URI.name(version), BufferUtil.toUTF8String(uri)); // TODO handle bad encodings |
| headers.put(HTTPSPDYHeader.VERSION.name(version), httpVersion.asString()); |
| return false; |
| } |
| |
| @Override |
| public boolean parsedHeader(HttpField field) |
| { |
| if (field.getHeader() == HttpHeader.HOST) |
| headers.put(HTTPSPDYHeader.HOST.name(version), field.getValue()); |
| else |
| headers.put(field.getName(), field.getValue()); |
| return false; |
| } |
| |
| @Override |
| public boolean parsedHostHeader(String host, int port) |
| { |
| return false; |
| } |
| |
| @Override |
| public boolean headerComplete() |
| { |
| return false; |
| } |
| |
| @Override |
| public boolean content(ByteBuffer item) |
| { |
| if (content == null) |
| { |
| stream = syn(false); |
| content = item; |
| } |
| else |
| { |
| stream.getStreamFrameListener().onData(stream, toDataInfo(item, false)); |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean messageComplete() |
| { |
| if (stream == null) |
| { |
| assert content == null; |
| if (headers.isEmpty()) |
| proxyEngineSelector.onGoAway(session, new GoAwayResultInfo(0, SessionStatus.OK)); |
| else |
| syn(true); |
| } |
| else |
| { |
| stream.getStreamFrameListener().onData(stream, toDataInfo(content, true)); |
| } |
| return false; |
| } |
| |
| @Override |
| public void completed() |
| { |
| headers.clear(); |
| stream = null; |
| content = null; |
| super.completed(); |
| } |
| |
| @Override |
| public int getHeaderCacheSize() |
| { |
| // TODO get from configuration |
| return 256; |
| } |
| |
| @Override |
| public void earlyEOF() |
| { |
| // TODO |
| } |
| |
| @Override |
| public void badMessage(int status, String reason) |
| { |
| // TODO |
| } |
| |
| private HTTPStream syn(boolean close) |
| { |
| HTTPStream stream = new HTTPStream(1, (byte)0, session, null); |
| StreamFrameListener streamFrameListener = proxyEngineSelector.onSyn(stream, new SynInfo(headers, close)); |
| stream.setStreamFrameListener(streamFrameListener); |
| return stream; |
| } |
| |
| private DataInfo toDataInfo(ByteBuffer buffer, boolean close) |
| { |
| return new ByteBufferDataInfo(buffer, close); |
| } |
| |
| private class HTTPSession extends StandardSession |
| { |
| private HTTPSession(short version, Connector connector) |
| { |
| super(version, connector.getByteBufferPool(), connector.getScheduler(), null, |
| getEndPoint(), null, 1, proxyEngineSelector, null, null); |
| } |
| |
| @Override |
| public void rst(RstInfo rstInfo, Callback handler) |
| { |
| HttpGenerator.ResponseInfo info = new HttpGenerator.ResponseInfo(HttpVersion.fromString(headers.get |
| ("version").getValue()), null, 0, 502, "SPDY reset received from upstream server", false); |
| send(info, null, true, Callback.Adapter.INSTANCE); |
| } |
| |
| @Override |
| public void goAway(GoAwayInfo goAwayInfo, Callback handler) |
| { |
| ProxyHTTPSPDYConnection.this.close(); |
| handler.succeeded(); |
| } |
| } |
| |
| /** |
| * <p>This stream will convert the SPDY invocations performed by the proxy into HTTP to be sent to the client.</p> |
| */ |
| private class HTTPStream extends StandardStream |
| { |
| private final Pattern statusRegexp = Pattern.compile("(\\d{3})\\s+(.*)"); |
| |
| private HTTPStream(int id, byte priority, ISession session, IStream associatedStream) |
| { |
| super(id, priority, session, associatedStream, getHttpChannel().getScheduler(), null); |
| } |
| |
| @Override |
| public void push(PushInfo pushInfo, Promise<Stream> handler) |
| { |
| // HTTP does not support pushed streams |
| handler.succeeded(new HTTPPushStream(2, getPriority(), getSession(), this)); |
| } |
| |
| @Override |
| public void headers(HeadersInfo headersInfo, Callback handler) |
| { |
| // TODO |
| throw new UnsupportedOperationException("Not Yet Implemented"); |
| } |
| |
| @Override |
| public void reply(ReplyInfo replyInfo, final Callback handler) |
| { |
| Fields headers = new Fields(replyInfo.getHeaders(), false); |
| |
| addPersistenceHeader(headers); |
| |
| headers.remove(HTTPSPDYHeader.SCHEME.name(version)); |
| |
| String status = headers.remove(HTTPSPDYHeader.STATUS.name(version)).getValue(); |
| Matcher matcher = statusRegexp.matcher(status); |
| matcher.matches(); |
| int code = Integer.parseInt(matcher.group(1)); |
| String reason = matcher.group(2).trim(); |
| |
| HttpVersion httpVersion = HttpVersion.fromString(headers.remove(HTTPSPDYHeader.VERSION.name(version)).getValue()); |
| |
| // Convert the Host header from a SPDY special header to a normal header |
| Fields.Field host = headers.remove(HTTPSPDYHeader.HOST.name(version)); |
| if (host != null) |
| headers.put("host", host.getValue()); |
| |
| HttpFields fields = new HttpFields(); |
| for (Fields.Field header : headers) |
| { |
| String name = camelize(header.getName()); |
| fields.put(name, header.getValue()); |
| } |
| |
| // TODO: handle better the HEAD last parameter |
| long contentLength = fields.getLongField(HttpHeader.CONTENT_LENGTH.asString()); |
| HttpGenerator.ResponseInfo info = new HttpGenerator.ResponseInfo(httpVersion, fields, contentLength, code, |
| reason, false); |
| |
| send(info, null, replyInfo.isClose(), new Adapter() |
| { |
| @Override |
| public void failed(Throwable x) |
| { |
| handler.failed(x); |
| } |
| }); |
| |
| if (replyInfo.isClose()) |
| completed(); |
| |
| handler.succeeded(); |
| } |
| |
| private String camelize(String name) |
| { |
| char[] chars = name.toCharArray(); |
| chars[0] = Character.toUpperCase(chars[0]); |
| |
| for (int i = 0; i < chars.length; ++i) |
| { |
| char c = chars[i]; |
| int j = i + 1; |
| if (c == '-' && j < chars.length) |
| chars[j] = Character.toUpperCase(chars[j]); |
| } |
| return new String(chars); |
| } |
| |
| @Override |
| public void data(DataInfo dataInfo, final Callback handler) |
| { |
| // Data buffer must be copied, as the ByteBuffer is pooled |
| ByteBuffer byteBuffer = dataInfo.asByteBuffer(false); |
| |
| send(null, byteBuffer, dataInfo.isClose(), new Adapter() |
| { |
| @Override |
| public void failed(Throwable x) |
| { |
| handler.failed(x); |
| } |
| }); |
| |
| if (dataInfo.isClose()) |
| completed(); |
| |
| handler.succeeded(); |
| } |
| } |
| |
| private void addPersistenceHeader(Fields headersToAddTo) |
| { |
| HttpVersion httpVersion = HttpVersion.fromString(headers.get("version").getValue()); |
| boolean persistent = false; |
| switch (httpVersion) |
| { |
| case HTTP_1_0: |
| { |
| Fields.Field keepAliveHeader = headers.get(HttpHeader.KEEP_ALIVE.asString()); |
| if(keepAliveHeader!=null) |
| persistent = HttpHeaderValue.KEEP_ALIVE.asString().equals(keepAliveHeader.getValue()); |
| if (!persistent) |
| persistent = HttpMethod.CONNECT.is(headers.get("method").getValue()); |
| if (persistent) |
| headersToAddTo.add(HttpHeader.CONNECTION.asString(), HttpHeaderValue.KEEP_ALIVE.asString()); |
| break; |
| } |
| case HTTP_1_1: |
| { |
| Fields.Field connectionHeader = headers.get(HttpHeader.CONNECTION.asString()); |
| if(connectionHeader != null) |
| persistent = !HttpHeaderValue.CLOSE.asString().equals(connectionHeader.getValue()); |
| else |
| persistent = true; |
| if (!persistent) |
| persistent = HttpMethod.CONNECT.is(headers.get("method").getValue()); |
| if (!persistent) |
| headersToAddTo.add(HttpHeader.CONNECTION.asString(), HttpHeaderValue.CLOSE.asString()); |
| break; |
| } |
| default: |
| { |
| throw new IllegalStateException(); |
| } |
| } |
| } |
| |
| private class HTTPPushStream extends StandardStream |
| { |
| private HTTPPushStream(int id, byte priority, ISession session, IStream associatedStream) |
| { |
| super(id, priority, session, associatedStream, getHttpChannel().getScheduler(), null); |
| } |
| |
| @Override |
| public void headers(HeadersInfo headersInfo, Callback handler) |
| { |
| // Ignore pushed headers |
| handler.succeeded(); |
| } |
| |
| @Override |
| public void data(DataInfo dataInfo, Callback handler) |
| { |
| // Ignore pushed data |
| handler.succeeded(); |
| } |
| } |
| } |