View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2017 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.websocket.client.io;
20  
21  import java.io.EOFException;
22  import java.io.IOException;
23  import java.net.URI;
24  import java.nio.ByteBuffer;
25  import java.nio.charset.StandardCharsets;
26  import java.util.ArrayList;
27  import java.util.List;
28  import java.util.concurrent.Executor;
29  
30  import org.eclipse.jetty.io.AbstractConnection;
31  import org.eclipse.jetty.io.ByteBufferPool;
32  import org.eclipse.jetty.io.EndPoint;
33  import org.eclipse.jetty.util.BufferUtil;
34  import org.eclipse.jetty.util.FutureCallback;
35  import org.eclipse.jetty.util.QuotedStringTokenizer;
36  import org.eclipse.jetty.util.log.Log;
37  import org.eclipse.jetty.util.log.Logger;
38  import org.eclipse.jetty.websocket.api.UpgradeException;
39  import org.eclipse.jetty.websocket.api.WebSocketPolicy;
40  import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
41  import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
42  import org.eclipse.jetty.websocket.client.ClientUpgradeResponse;
43  import org.eclipse.jetty.websocket.common.AcceptHash;
44  import org.eclipse.jetty.websocket.common.SessionFactory;
45  import org.eclipse.jetty.websocket.common.WebSocketSession;
46  import org.eclipse.jetty.websocket.common.events.EventDriver;
47  import org.eclipse.jetty.websocket.common.extensions.ExtensionStack;
48  import org.eclipse.jetty.websocket.common.io.http.HttpResponseHeaderParser;
49  import org.eclipse.jetty.websocket.common.io.http.HttpResponseHeaderParser.ParseException;
50  
51  /**
52   * This is the initial connection handling that exists immediately after physical connection is established to destination server.
53   * <p>
54   * Eventually, upon successful Upgrade request/response, this connection swaps itself out for the WebSocektClientConnection handler.
55   */
56  public class UpgradeConnection extends AbstractConnection
57  {
58      public class SendUpgradeRequest extends FutureCallback implements Runnable
59      {
60          @Override
61          public void run()
62          {
63              URI uri = connectPromise.getRequest().getRequestURI();
64              request.setRequestURI(uri);
65  
66              UpgradeListener handshakeListener = connectPromise.getUpgradeListener();
67              if (handshakeListener != null)
68              {
69                  handshakeListener.onHandshakeRequest(request);
70              }
71  
72              String rawRequest = request.generate();
73  
74              ByteBuffer buf = BufferUtil.toBuffer(rawRequest, StandardCharsets.UTF_8);
75              getEndPoint().write(this,buf);
76          }
77  
78          @Override
79          public void succeeded()
80          {
81              LOG.debug("Upgrade Request Write Success");
82              // Writing the request header is complete.
83              super.succeeded();
84              // start the interest in fill
85              fillInterested();
86          }
87  
88          @Override
89          public void failed(Throwable cause)
90          {
91              LOG.warn("Upgrade Request Write Failure", cause);
92              super.failed(cause);
93              // Fail the connect promise when a fundamental exception during connect occurs.
94              connectPromise.failed(cause);
95          }
96      }
97  
98      /** HTTP Response Code: 101 Switching Protocols */
99      private static final int SWITCHING_PROTOCOLS = 101;
100 
101     private static final Logger LOG = Log.getLogger(UpgradeConnection.class);
102     private final ByteBufferPool bufferPool;
103     private final ConnectPromise connectPromise;
104     private final HttpResponseHeaderParser parser;
105     private ClientUpgradeRequest request;
106 
107     public UpgradeConnection(EndPoint endp, Executor executor, ConnectPromise connectPromise)
108     {
109         super(endp,executor);
110         this.connectPromise = connectPromise;
111         this.bufferPool = connectPromise.getClient().getBufferPool();
112         this.request = connectPromise.getRequest();
113 
114         // Setup the parser
115         this.parser = new HttpResponseHeaderParser(new ClientUpgradeResponse());
116     }
117 
118     public void disconnect(boolean onlyOutput)
119     {
120         EndPoint endPoint = getEndPoint();
121         // We need to gently close first, to allow
122         // SSL close alerts to be sent by Jetty
123         if (LOG.isDebugEnabled())
124             LOG.debug("Shutting down output {}",endPoint);
125         endPoint.shutdownOutput();
126         if (!onlyOutput)
127         {
128             if (LOG.isDebugEnabled())
129                 LOG.debug("Closing {}",endPoint);
130             endPoint.close();
131         }
132     }
133 
134     private void failUpgrade(Throwable cause)
135     {
136         close();
137         connectPromise.failed(cause);
138     }
139 
140     private void notifyConnect(ClientUpgradeResponse response)
141     {
142         connectPromise.setResponse(response);
143 
144         UpgradeListener handshakeListener = connectPromise.getUpgradeListener();
145         if (handshakeListener != null)
146         {
147             handshakeListener.onHandshakeResponse(response);
148         }
149     }
150 
151     @Override
152     public void onFillable()
153     {
154         if (LOG.isDebugEnabled())
155         {
156             LOG.debug("onFillable");
157         }
158         ByteBuffer buffer = bufferPool.acquire(getInputBufferSize(),false);
159         BufferUtil.clear(buffer);
160         boolean readMore = false;
161         try
162         {
163             readMore = read(buffer);
164         }
165         finally
166         {
167             bufferPool.release(buffer);
168         }
169 
170         if (readMore)
171         {
172             fillInterested();
173         }
174     }
175 
176     @Override
177     public void onOpen()
178     {
179         super.onOpen();
180         getExecutor().execute(new SendUpgradeRequest());
181     }
182     
183     @Override
184     public void onClose()
185     {
186         if (LOG.isDebugEnabled())
187         {
188             LOG.warn("Closed connection {}",this);
189         }
190         super.onClose();
191     }
192     
193     @Override
194     protected boolean onReadTimeout()
195     {
196         if (LOG.isDebugEnabled())
197         {
198             LOG.warn("Timeout on connection {}",this);
199         }
200         
201         failUpgrade(new IOException("Timeout while performing WebSocket Upgrade"));
202         
203         return super.onReadTimeout();
204     }
205 
206     /**
207      * Read / Parse the waiting read/fill buffer
208      * 
209      * @param buffer
210      *            the buffer to fill into from the endpoint
211      * @return true if there is more to read, false if reading should stop
212      */
213     private boolean read(ByteBuffer buffer)
214     {
215         EndPoint endPoint = getEndPoint();
216         try
217         {
218             while (true)
219             {
220                 int filled = endPoint.fill(buffer);
221                 if (filled == 0)
222                 {
223                     return true;
224                 }
225                 else if (filled < 0)
226                 {
227                     LOG.warn("read - EOF Reached");
228                     failUpgrade(new EOFException("Reading WebSocket Upgrade response"));
229                     return false;
230                 }
231                 else
232                 {
233                     if (LOG.isDebugEnabled())
234                     {
235                         LOG.debug("Filled {} bytes - {}",filled,BufferUtil.toDetailString(buffer));
236                     }
237                     ClientUpgradeResponse resp = (ClientUpgradeResponse)parser.parse(buffer);
238                     if (resp != null)
239                     {
240                         // Got a response!
241                         validateResponse(resp);
242                         notifyConnect(resp);
243                         upgradeConnection(resp);
244                         if (buffer.hasRemaining())
245                         {
246                             LOG.debug("Has remaining client bytebuffer of {}",buffer.remaining());
247                         }
248                         return false; // do no more reading
249                     }
250                 }
251             }
252         }
253         catch (IOException | ParseException e)
254         {
255             UpgradeException ue = new UpgradeException(request.getRequestURI(),e);
256             connectPromise.failed(ue);
257             disconnect(false);
258             return false;
259         }
260         catch (UpgradeException e)
261         {
262             connectPromise.failed(e);
263             disconnect(false);
264             return false;
265         }
266     }
267 
268     private void upgradeConnection(ClientUpgradeResponse response)
269     {
270         EndPoint endp = getEndPoint();
271         Executor executor = getExecutor();
272         
273         EventDriver websocket = connectPromise.getDriver();
274         WebSocketPolicy policy = websocket.getPolicy();
275 
276         WebSocketClientConnection connection = new WebSocketClientConnection(endp,executor,connectPromise,policy);
277 
278         SessionFactory sessionFactory = connectPromise.getClient().getSessionFactory();
279         WebSocketSession session = sessionFactory.createSession(request.getRequestURI(),websocket,connection);
280         session.setPolicy(policy);
281         session.setUpgradeResponse(response);
282 
283         connection.setSession(session);
284 
285         // Initialize / Negotiate Extensions
286         ExtensionStack extensionStack = new ExtensionStack(connectPromise.getClient().getExtensionFactory());
287         extensionStack.negotiate(response.getExtensions());
288 
289         extensionStack.configure(connection.getParser());
290         extensionStack.configure(connection.getGenerator());
291 
292         // Setup Incoming Routing
293         connection.setNextIncomingFrames(extensionStack);
294         extensionStack.setNextIncoming(session);
295 
296         // Setup Outgoing Routing
297         session.setOutgoingHandler(extensionStack);
298         extensionStack.setNextOutgoing(connection);
299 
300         session.addBean(extensionStack);
301         connectPromise.getClient().addManaged(session);
302 
303         // Now swap out the connection
304         // TODO use endp.upgrade ???
305         endp.setConnection(connection);
306         connection.onOpen();
307     }
308 
309     private void validateResponse(ClientUpgradeResponse response)
310     {
311         // Validate Response Status Code
312         if (response.getStatusCode() != SWITCHING_PROTOCOLS)
313         {
314             throw new UpgradeException(request.getRequestURI(),response.getStatusCode(),"Didn't switch protocols");
315         }
316 
317         // Validate Connection header
318         String connection = response.getHeader("Connection");
319         if (!"upgrade".equalsIgnoreCase(connection))
320         {
321             throw new UpgradeException(request.getRequestURI(),response.getStatusCode(),"Connection is " + connection + " (expected upgrade)");
322         }
323 
324         // Check the Accept hash
325         String reqKey = request.getKey();
326         String expectedHash = AcceptHash.hashKey(reqKey);
327         String respHash = response.getHeader("Sec-WebSocket-Accept");
328 
329         response.setSuccess(true);
330         if (expectedHash.equalsIgnoreCase(respHash) == false)
331         {
332             response.setSuccess(false);
333             throw new UpgradeException(request.getRequestURI(),response.getStatusCode(),"Invalid Sec-WebSocket-Accept hash");
334         }
335 
336         // Parse extensions
337         List<ExtensionConfig> extensions = new ArrayList<>();
338         List<String> extValues = response.getHeaders("Sec-WebSocket-Extensions");
339         if (extValues != null)
340         {
341             for (String extVal : extValues)
342             {
343                 QuotedStringTokenizer tok = new QuotedStringTokenizer(extVal,",");
344                 while (tok.hasMoreTokens())
345                 {
346                     extensions.add(ExtensionConfig.parse(tok.nextToken()));
347                 }
348             }
349         }
350         response.setExtensions(extensions);
351     }
352 }