View Javadoc

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