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.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.server.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         headers.clear();
148         stream = null;
149         content = null;
150         return false;
151     }
152 
153     @Override
154     public boolean earlyEOF()
155     {
156         // TODO
157         return false;
158     }
159 
160     @Override
161     public void badMessage(int status, String reason)
162     {
163         // TODO
164     }
165 
166     private HTTPStream syn(boolean close)
167     {
168         HTTPStream stream = new HTTPStream(1, (byte)0, session, null);
169         StreamFrameListener streamFrameListener = proxyEngineSelector.onSyn(stream, new SynInfo(headers, close));
170         stream.setStreamFrameListener(streamFrameListener);
171         return stream;
172     }
173 
174     private DataInfo toDataInfo(ByteBuffer buffer, boolean close)
175     {
176         return new ByteBufferDataInfo(buffer, close);
177     }
178 
179     private class HTTPSession extends StandardSession
180     {
181         private HTTPSession(short version, Connector connector)
182         {
183             super(version, connector.getByteBufferPool(), connector.getExecutor(), connector.getScheduler(), null,
184                     getEndPoint(), null, 1, proxyEngineSelector, null, null);
185         }
186 
187         @Override
188         public void rst(RstInfo rstInfo, Callback handler)
189         {
190             // Not much we can do in HTTP land: just close the connection
191             goAway(new GoAwayInfo(rstInfo.getTimeout(), rstInfo.getUnit()), handler);
192         }
193 
194         @Override
195         public void goAway(GoAwayInfo goAwayInfo, Callback handler)
196         {
197             getEndPoint().close();
198             handler.succeeded();
199         }
200     }
201 
202     /**
203      * <p>This stream will convert the SPDY invocations performed by the proxy into HTTP to be sent to the client.</p>
204      */
205     private class HTTPStream extends StandardStream
206     {
207         private final Pattern statusRegexp = Pattern.compile("(\\d{3})\\s+(.*)");
208 
209         private HTTPStream(int id, byte priority, ISession session, IStream associatedStream)
210         {
211             super(id, priority, session, associatedStream, null);
212         }
213 
214         @Override
215         public void push(PushInfo pushInfo, Promise<Stream> handler)
216         {
217             // HTTP does not support pushed streams
218             handler.succeeded(new HTTPPushStream(2, getPriority(), getSession(), this));
219         }
220 
221         @Override
222         public void headers(HeadersInfo headersInfo, Callback handler)
223         {
224             // TODO
225             throw new UnsupportedOperationException("Not Yet Implemented");
226         }
227 
228         @Override
229         public void reply(ReplyInfo replyInfo, Callback handler)
230         {
231             try
232             {
233                 Fields headers = new Fields(replyInfo.getHeaders(), false);
234 
235                 headers.remove(HTTPSPDYHeader.SCHEME.name(version));
236 
237                 String status = headers.remove(HTTPSPDYHeader.STATUS.name(version)).value();
238                 Matcher matcher = statusRegexp.matcher(status);
239                 matcher.matches();
240                 int code = Integer.parseInt(matcher.group(1));
241                 String reason = matcher.group(2).trim();
242 
243                 HttpVersion httpVersion = HttpVersion.fromString(headers.remove(HTTPSPDYHeader.VERSION.name(version)).value());
244 
245                 // Convert the Host header from a SPDY special header to a normal header
246                 Fields.Field host = headers.remove(HTTPSPDYHeader.HOST.name(version));
247                 if (host != null)
248                     headers.put("host", host.value());
249 
250                 HttpFields fields = new HttpFields();
251                 for (Fields.Field header : headers)
252                 {
253                     String name = camelize(header.name());
254                     fields.put(name, header.value());
255                 }
256 
257                 // TODO: handle better the HEAD last parameter
258                 long contentLength = fields.getLongField(HttpHeader.CONTENT_LENGTH.asString());
259                 HttpGenerator.ResponseInfo info = new HttpGenerator.ResponseInfo(httpVersion, fields, contentLength, code,
260                         reason, false);
261                 send(info, null, replyInfo.isClose());
262 
263                 if (replyInfo.isClose())
264                     completed();
265 
266                 handler.succeeded();
267             }
268             catch (IOException x)
269             {
270                 handler.failed(x);
271             }
272         }
273 
274         private String camelize(String name)
275         {
276             char[] chars = name.toCharArray();
277             chars[0] = Character.toUpperCase(chars[0]);
278 
279             for (int i = 0; i < chars.length; ++i)
280             {
281                 char c = chars[i];
282                 int j = i + 1;
283                 if (c == '-' && j < chars.length)
284                     chars[j] = Character.toUpperCase(chars[j]);
285             }
286             return new String(chars);
287         }
288 
289         @Override
290         public void data(DataInfo dataInfo, Callback handler)
291         {
292             try
293             {
294                 // Data buffer must be copied, as the ByteBuffer is pooled
295                 ByteBuffer byteBuffer = dataInfo.asByteBuffer(false);
296 
297                 send(null, byteBuffer, dataInfo.isClose());
298 
299                 if (dataInfo.isClose())
300                     completed();
301 
302                 handler.succeeded();
303             }
304             catch (IOException x)
305             {
306                 handler.failed(x);
307             }
308         }
309     }
310 
311     private class HTTPPushStream extends StandardStream
312     {
313         private HTTPPushStream(int id, byte priority, ISession session, IStream associatedStream)
314         {
315             super(id, priority, session, associatedStream, null);
316         }
317 
318         @Override
319         public void headers(HeadersInfo headersInfo, Callback handler)
320         {
321             // Ignore pushed headers
322             handler.succeeded();
323         }
324 
325         @Override
326         public void data(DataInfo dataInfo, Callback handler)
327         {
328             // Ignore pushed data
329             handler.succeeded();
330         }
331     }
332 }