View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.spdy.server.proxy;
20  
21  import java.io.IOException;
22  import java.nio.ByteBuffer;
23  import java.util.regex.Matcher;
24  import java.util.regex.Pattern;
25  
26  import org.eclipse.jetty.http.HttpField;
27  import org.eclipse.jetty.http.HttpFields;
28  import org.eclipse.jetty.http.HttpGenerator;
29  import org.eclipse.jetty.http.HttpHeader;
30  import org.eclipse.jetty.http.HttpHeaderValue;
31  import org.eclipse.jetty.http.HttpMethod;
32  import org.eclipse.jetty.http.HttpParser;
33  import org.eclipse.jetty.http.HttpVersion;
34  import org.eclipse.jetty.io.EndPoint;
35  import org.eclipse.jetty.server.Connector;
36  import org.eclipse.jetty.server.HttpConfiguration;
37  import org.eclipse.jetty.server.HttpConnection;
38  import org.eclipse.jetty.server.SslConnectionFactory;
39  import org.eclipse.jetty.spdy.ISession;
40  import org.eclipse.jetty.spdy.IStream;
41  import org.eclipse.jetty.spdy.StandardSession;
42  import org.eclipse.jetty.spdy.StandardStream;
43  import org.eclipse.jetty.spdy.api.ByteBufferDataInfo;
44  import org.eclipse.jetty.spdy.api.DataInfo;
45  import org.eclipse.jetty.spdy.api.GoAwayInfo;
46  import org.eclipse.jetty.spdy.api.GoAwayResultInfo;
47  import org.eclipse.jetty.spdy.api.HeadersInfo;
48  import org.eclipse.jetty.spdy.api.PushInfo;
49  import org.eclipse.jetty.spdy.api.ReplyInfo;
50  import org.eclipse.jetty.spdy.api.RstInfo;
51  import org.eclipse.jetty.spdy.api.SessionStatus;
52  import org.eclipse.jetty.spdy.api.Stream;
53  import org.eclipse.jetty.spdy.api.StreamFrameListener;
54  import org.eclipse.jetty.spdy.api.SynInfo;
55  import org.eclipse.jetty.spdy.server.http.HTTPSPDYHeader;
56  import org.eclipse.jetty.util.BufferUtil;
57  import org.eclipse.jetty.util.Callback;
58  import org.eclipse.jetty.util.Fields;
59  import org.eclipse.jetty.util.Promise;
60  
61  public class ProxyHTTPSPDYConnection extends HttpConnection implements HttpParser.RequestHandler<ByteBuffer>
62  {
63      private final short version;
64      private final Fields headers = new Fields();
65      private final ProxyEngineSelector proxyEngineSelector;
66      private final ISession session;
67      private HTTPStream stream;
68      private ByteBuffer content;
69  
70      public ProxyHTTPSPDYConnection(Connector connector, HttpConfiguration config, EndPoint endPoint, short version, ProxyEngineSelector proxyEngineSelector)
71      {
72          super(config, connector, endPoint);
73          this.version = version;
74          this.proxyEngineSelector = proxyEngineSelector;
75          this.session = new HTTPSession(version, connector);
76      }
77  
78      @Override
79      protected HttpParser.RequestHandler<ByteBuffer> newRequestHandler()
80      {
81          return this;
82      }
83  
84      @Override
85      public boolean startRequest(HttpMethod method, String methodString, ByteBuffer uri, HttpVersion httpVersion)
86      {
87          Connector connector = getConnector();
88          String scheme = connector.getConnectionFactory(SslConnectionFactory.class) != null ? "https" : "http";
89          headers.put(HTTPSPDYHeader.SCHEME.name(version), scheme);
90          headers.put(HTTPSPDYHeader.METHOD.name(version), methodString);
91          headers.put(HTTPSPDYHeader.URI.name(version), BufferUtil.toUTF8String(uri)); // TODO handle bad encodings
92          headers.put(HTTPSPDYHeader.VERSION.name(version), httpVersion.asString());
93          return false;
94      }
95  
96      @Override
97      public boolean parsedHeader(HttpField field)
98      {
99          if (field.getHeader() == HttpHeader.HOST)
100             headers.put(HTTPSPDYHeader.HOST.name(version), field.getValue());
101         else
102             headers.put(field.getName(), field.getValue());
103         return false;
104     }
105 
106     @Override
107     public boolean parsedHostHeader(String host, int port)
108     {
109         return false;
110     }
111 
112     @Override
113     public boolean headerComplete()
114     {
115         return false;
116     }
117 
118     @Override
119     public boolean content(ByteBuffer item)
120     {
121         if (content == null)
122         {
123             stream = syn(false);
124             content = item;
125         }
126         else
127         {
128             stream.getStreamFrameListener().onData(stream, toDataInfo(item, false));
129         }
130         return false;
131     }
132 
133     @Override
134     public boolean messageComplete()
135     {
136         if (stream == null)
137         {
138             assert content == null;
139             if (headers.isEmpty())
140                 proxyEngineSelector.onGoAway(session, new GoAwayResultInfo(0, SessionStatus.OK));
141             else
142                 syn(true);
143         }
144         else
145         {
146             stream.getStreamFrameListener().onData(stream, toDataInfo(content, true));
147         }
148         return false;
149     }
150 
151     @Override
152     public void completed()
153     {
154         headers.clear();
155         stream = null;
156         content = null;
157         super.completed();
158     }
159 
160     @Override
161     public int getHeaderCacheSize()
162     {
163         // TODO get from configuration
164         return 256;
165     }
166 
167     @Override
168     public void earlyEOF()
169     {
170         // TODO
171     }
172 
173     @Override
174     public void badMessage(int status, String reason)
175     {
176         // TODO
177     }
178 
179     private HTTPStream syn(boolean close)
180     {
181         HTTPStream stream = new HTTPStream(1, (byte)0, session, null);
182         StreamFrameListener streamFrameListener = proxyEngineSelector.onSyn(stream, new SynInfo(headers, close));
183         stream.setStreamFrameListener(streamFrameListener);
184         return stream;
185     }
186 
187     private DataInfo toDataInfo(ByteBuffer buffer, boolean close)
188     {
189         return new ByteBufferDataInfo(buffer, close);
190     }
191 
192     private class HTTPSession extends StandardSession
193     {
194         private HTTPSession(short version, Connector connector)
195         {
196             super(version, connector.getByteBufferPool(), connector.getExecutor(), connector.getScheduler(), null,
197                     getEndPoint(), null, 1, proxyEngineSelector, null, null);
198         }
199 
200         @Override
201         public void rst(RstInfo rstInfo, Callback handler)
202         {
203             HttpGenerator.ResponseInfo info = new HttpGenerator.ResponseInfo(HttpVersion.fromString(headers.get
204                     ("version").value()), null, 0, 502, "SPDY reset received from upstream server", false);
205             send(info, null, true, new Callback.Adapter());
206         }
207 
208         @Override
209         public void goAway(GoAwayInfo goAwayInfo, Callback handler)
210         {
211             ProxyHTTPSPDYConnection.this.close();
212             handler.succeeded();
213         }
214     }
215 
216     /**
217      * <p>This stream will convert the SPDY invocations performed by the proxy into HTTP to be sent to the client.</p>
218      */
219     private class HTTPStream extends StandardStream
220     {
221         private final Pattern statusRegexp = Pattern.compile("(\\d{3})\\s+(.*)");
222 
223         private HTTPStream(int id, byte priority, ISession session, IStream associatedStream)
224         {
225             super(id, priority, session, associatedStream, null);
226         }
227 
228         @Override
229         public void push(PushInfo pushInfo, Promise<Stream> handler)
230         {
231             // HTTP does not support pushed streams
232             handler.succeeded(new HTTPPushStream(2, getPriority(), getSession(), this));
233         }
234 
235         @Override
236         public void headers(HeadersInfo headersInfo, Callback handler)
237         {
238             // TODO
239             throw new UnsupportedOperationException("Not Yet Implemented");
240         }
241 
242         @Override
243         public void reply(ReplyInfo replyInfo, Callback handler)
244         {
245             try
246             {
247                 Fields headers = new Fields(replyInfo.getHeaders(), false);
248 
249                 addPersistenceHeader(headers);
250 
251                 headers.remove(HTTPSPDYHeader.SCHEME.name(version));
252 
253                 String status = headers.remove(HTTPSPDYHeader.STATUS.name(version)).value();
254                 Matcher matcher = statusRegexp.matcher(status);
255                 matcher.matches();
256                 int code = Integer.parseInt(matcher.group(1));
257                 String reason = matcher.group(2).trim();
258 
259                 HttpVersion httpVersion = HttpVersion.fromString(headers.remove(HTTPSPDYHeader.VERSION.name(version)).value());
260 
261                 // Convert the Host header from a SPDY special header to a normal header
262                 Fields.Field host = headers.remove(HTTPSPDYHeader.HOST.name(version));
263                 if (host != null)
264                     headers.put("host", host.value());
265 
266                 HttpFields fields = new HttpFields();
267                 for (Fields.Field header : headers)
268                 {
269                     String name = camelize(header.name());
270                     fields.put(name, header.value());
271                 }
272 
273                 // TODO: handle better the HEAD last parameter
274                 long contentLength = fields.getLongField(HttpHeader.CONTENT_LENGTH.asString());
275                 HttpGenerator.ResponseInfo info = new HttpGenerator.ResponseInfo(httpVersion, fields, contentLength, code,
276                         reason, false);
277 
278                 // TODO use the async send 
279                 send(info, null, replyInfo.isClose());
280 
281                 if (replyInfo.isClose())
282                     completed();
283 
284                 handler.succeeded();
285             }
286             catch (IOException x)
287             {
288                 handler.failed(x);
289             }
290         }
291 
292         private String camelize(String name)
293         {
294             char[] chars = name.toCharArray();
295             chars[0] = Character.toUpperCase(chars[0]);
296 
297             for (int i = 0; i < chars.length; ++i)
298             {
299                 char c = chars[i];
300                 int j = i + 1;
301                 if (c == '-' && j < chars.length)
302                     chars[j] = Character.toUpperCase(chars[j]);
303             }
304             return new String(chars);
305         }
306 
307         @Override
308         public void data(DataInfo dataInfo, Callback handler)
309         {
310             try
311             {
312                 // Data buffer must be copied, as the ByteBuffer is pooled
313                 ByteBuffer byteBuffer = dataInfo.asByteBuffer(false);
314 
315                 // TODO use the async send with callback!
316                 send(null, byteBuffer, dataInfo.isClose());
317 
318                 if (dataInfo.isClose())
319                     completed();
320 
321                 handler.succeeded();
322             }
323             catch (IOException x)
324             {
325                 handler.failed(x);
326             }
327         }
328     }
329 
330     private void addPersistenceHeader(Fields headersToAddTo)
331     {
332         HttpVersion httpVersion = HttpVersion.fromString(headers.get("version").value());
333         boolean persistent = false;
334         switch (httpVersion)
335         {
336             case HTTP_1_0:
337             {
338                 Fields.Field keepAliveHeader = headers.get(HttpHeader.KEEP_ALIVE.asString());
339                 if (keepAliveHeader != null)
340                     persistent = HttpHeaderValue.KEEP_ALIVE.asString().equals(keepAliveHeader.value());
341                 if (!persistent)
342                     persistent = HttpMethod.CONNECT.is(headers.get("method").value());
343                 if (persistent)
344                     headersToAddTo.add(HttpHeader.CONNECTION.asString(), HttpHeaderValue.KEEP_ALIVE.asString());
345                 break;
346             }
347             case HTTP_1_1:
348             {
349                 Fields.Field connectionHeader = headers.get(HttpHeader.CONNECTION.asString());
350                 if (connectionHeader != null)
351                     persistent = !HttpHeaderValue.CLOSE.asString().equals(connectionHeader.value());
352                 else
353                     persistent = true;
354                 if (!persistent)
355                     persistent = HttpMethod.CONNECT.is(headers.get("method").value());
356                 if (!persistent)
357                     headersToAddTo.add(HttpHeader.CONNECTION.asString(), HttpHeaderValue.CLOSE.asString());
358                 break;
359             }
360             default:
361             {
362                 throw new IllegalStateException();
363             }
364         }
365     }
366 
367     private class HTTPPushStream extends StandardStream
368     {
369         private HTTPPushStream(int id, byte priority, ISession session, IStream associatedStream)
370         {
371             super(id, priority, session, associatedStream, null);
372         }
373 
374         @Override
375         public void headers(HeadersInfo headersInfo, Callback handler)
376         {
377             // Ignore pushed headers
378             handler.succeeded();
379         }
380 
381         @Override
382         public void data(DataInfo dataInfo, Callback handler)
383         {
384             // Ignore pushed data
385             handler.succeeded();
386         }
387     }
388 }