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.client;
20  
21  import java.net.HttpCookie;
22  import java.net.URI;
23  import java.util.ArrayList;
24  import java.util.Enumeration;
25  import java.util.List;
26  import java.util.concurrent.TimeUnit;
27  import java.util.concurrent.atomic.AtomicReference;
28  
29  import org.eclipse.jetty.client.api.Authentication;
30  import org.eclipse.jetty.client.api.Connection;
31  import org.eclipse.jetty.client.api.ContentProvider;
32  import org.eclipse.jetty.client.api.Request;
33  import org.eclipse.jetty.client.api.Response;
34  import org.eclipse.jetty.http.HttpField;
35  import org.eclipse.jetty.http.HttpFields;
36  import org.eclipse.jetty.http.HttpHeader;
37  import org.eclipse.jetty.http.HttpHeaderValue;
38  import org.eclipse.jetty.http.HttpMethod;
39  import org.eclipse.jetty.http.HttpVersion;
40  import org.eclipse.jetty.io.AbstractConnection;
41  import org.eclipse.jetty.io.EndPoint;
42  import org.eclipse.jetty.util.log.Log;
43  import org.eclipse.jetty.util.log.Logger;
44  
45  public class HttpConnection extends AbstractConnection implements Connection
46  {
47      private static final Logger LOG = Log.getLogger(HttpConnection.class);
48      private static final HttpField CHUNKED_FIELD = new HttpField(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED);
49  
50      private final AtomicReference<HttpExchange> exchange = new AtomicReference<>();
51      private final HttpClient client;
52      private final HttpDestination destination;
53      private final HttpSender sender;
54      private final HttpReceiver receiver;
55      private long idleTimeout;
56      private volatile boolean closed;
57  
58      public HttpConnection(HttpClient client, EndPoint endPoint, HttpDestination destination)
59      {
60          super(endPoint, client.getExecutor(), client.isDispatchIO());
61          this.client = client;
62          this.destination = destination;
63          this.sender = new HttpSender(this);
64          this.receiver = new HttpReceiver(this);
65      }
66  
67      public HttpClient getHttpClient()
68      {
69          return client;
70      }
71  
72      public HttpDestination getDestination()
73      {
74          return destination;
75      }
76  
77      @Override
78      public void onOpen()
79      {
80          super.onOpen();
81          fillInterested();
82      }
83  
84      @Override
85      public void onClose()
86      {
87          closed = true;
88          super.onClose();
89      }
90  
91      @Override
92      public void fillInterested()
93      {
94          // This is necessary when "upgrading" the connection for example after proxied
95          // CONNECT requests, because the old connection will read the CONNECT response
96          // and then set the read interest, while the new connection attached to the same
97          // EndPoint also will set the read interest, causing a ReadPendingException.
98          if (!closed)
99              super.fillInterested();
100     }
101 
102     @Override
103     protected boolean onReadTimeout()
104     {
105         LOG.debug("{} idle timeout", this);
106 
107         HttpExchange exchange = getExchange();
108         if (exchange != null)
109             idleTimeout();
110         else
111             destination.remove(this);
112 
113         return true;
114     }
115 
116     protected void idleTimeout()
117     {
118         receiver.idleTimeout();
119     }
120 
121     @Override
122     public void send(Request request, Response.CompleteListener listener)
123     {
124         ArrayList<Response.ResponseListener> listeners = new ArrayList<>(2);
125         if (request.getTimeout() > 0)
126         {
127             TimeoutCompleteListener timeoutListener = new TimeoutCompleteListener(request);
128             timeoutListener.schedule(client.getScheduler());
129             listeners.add(timeoutListener);
130         }
131         if (listener != null)
132             listeners.add(listener);
133 
134         HttpConversation conversation = client.getConversation(request.getConversationID(), true);
135         HttpExchange exchange = new HttpExchange(conversation, getDestination(), request, listeners);
136         send(exchange);
137     }
138 
139     public void send(HttpExchange exchange)
140     {
141         Request request = exchange.getRequest();
142         normalizeRequest(request);
143 
144         // Save the old idle timeout to restore it
145         EndPoint endPoint = getEndPoint();
146         idleTimeout = endPoint.getIdleTimeout();
147         endPoint.setIdleTimeout(request.getIdleTimeout());
148 
149         // Associate the exchange to the connection
150         associate(exchange);
151 
152         sender.send(exchange);
153     }
154 
155     private void normalizeRequest(Request request)
156     {
157         if (request.getMethod() == null)
158             request.method(HttpMethod.GET);
159 
160         if (request.getVersion() == null)
161             request.version(HttpVersion.HTTP_1_1);
162 
163         if (request.getIdleTimeout() <= 0)
164             request.idleTimeout(client.getIdleTimeout(), TimeUnit.MILLISECONDS);
165 
166         HttpMethod method = request.getMethod();
167         HttpVersion version = request.getVersion();
168         HttpFields headers = request.getHeaders();
169         ContentProvider content = request.getContent();
170 
171         if (request.getAgent() == null)
172             headers.put(client.getUserAgentField());
173 
174         // Make sure the path is there
175         String path = request.getPath();
176         if (path.trim().length() == 0)
177         {
178             path = "/";
179             request.path(path);
180         }
181         if (destination.isProxied() && HttpMethod.CONNECT != method)
182         {
183             path = request.getURI().toString();
184             request.path(path);
185         }
186 
187         // If we are HTTP 1.1, add the Host header
188         if (version.getVersion() > 10)
189         {
190             if (!headers.containsKey(HttpHeader.HOST.asString()))
191                 headers.put(getDestination().getHostField());
192         }
193 
194         // Add content headers
195         if (content != null)
196         {
197             long contentLength = content.getLength();
198             if (contentLength >= 0)
199             {
200                 if (!headers.containsKey(HttpHeader.CONTENT_LENGTH.asString()))
201                     headers.put(HttpHeader.CONTENT_LENGTH, String.valueOf(contentLength));
202             }
203             else
204             {
205                 if (!headers.containsKey(HttpHeader.TRANSFER_ENCODING.asString()))
206                     headers.put(CHUNKED_FIELD);
207             }
208         }
209 
210         // Cookies
211         List<HttpCookie> cookies = client.getCookieStore().get(request.getURI());
212         StringBuilder cookieString = null;
213         for (int i = 0; i < cookies.size(); ++i)
214         {
215             if (cookieString == null)
216                 cookieString = new StringBuilder();
217             if (i > 0)
218                 cookieString.append("; ");
219             HttpCookie cookie = cookies.get(i);
220             cookieString.append(cookie.getName()).append("=").append(cookie.getValue());
221         }
222         if (cookieString != null)
223             request.header(HttpHeader.COOKIE.asString(), cookieString.toString());
224 
225         // Authorization
226         URI authenticationURI = destination.isProxied() ? destination.getProxyURI() : request.getURI();
227         Authentication.Result authnResult = client.getAuthenticationStore().findAuthenticationResult(authenticationURI);
228         if (authnResult != null)
229             authnResult.apply(request);
230 
231         if (!headers.containsKey(HttpHeader.ACCEPT_ENCODING.asString()))
232         {
233             HttpField acceptEncodingField = client.getAcceptEncodingField();
234             if (acceptEncodingField != null)
235                 headers.put(acceptEncodingField);
236         }
237     }
238 
239     public HttpExchange getExchange()
240     {
241         return exchange.get();
242     }
243 
244     protected void associate(HttpExchange exchange)
245     {
246         if (!this.exchange.compareAndSet(null, exchange))
247             throw new UnsupportedOperationException("Pipelined requests not supported");
248         exchange.setConnection(this);
249         LOG.debug("{} associated to {}", exchange, this);
250     }
251 
252     protected HttpExchange disassociate()
253     {
254         HttpExchange exchange = this.exchange.getAndSet(null);
255         if (exchange != null)
256             exchange.setConnection(null);
257         LOG.debug("{} disassociated from {}", exchange, this);
258         return exchange;
259     }
260 
261     @Override
262     public void onFillable()
263     {
264         HttpExchange exchange = getExchange();
265         if (exchange != null)
266         {
267             receive();
268         }
269         else
270         {
271             // If there is no exchange, then could be either a remote close,
272             // or garbage bytes; in both cases we close the connection
273             close();
274         }
275     }
276 
277     protected void receive()
278     {
279         receiver.receive();
280     }
281 
282     public void complete(HttpExchange exchange, boolean success)
283     {
284         HttpExchange existing = disassociate();
285         if (existing == exchange)
286         {
287             exchange.awaitTermination();
288 
289             // Restore idle timeout
290             getEndPoint().setIdleTimeout(idleTimeout);
291 
292             LOG.debug("{} disassociated from {}", exchange, this);
293             if (success)
294             {
295                 HttpFields responseHeaders = exchange.getResponse().getHeaders();
296                 Enumeration<String> values = responseHeaders.getValues(HttpHeader.CONNECTION.asString(), ",");
297                 if (values != null)
298                 {
299                     while (values.hasMoreElements())
300                     {
301                         if ("close".equalsIgnoreCase(values.nextElement()))
302                         {
303                             close();
304                             return;
305                         }
306                     }
307                 }
308                 destination.release(this);
309             }
310             else
311             {
312                 close();
313             }
314         }
315         else if (existing == null)
316         {
317             // It is possible that the exchange has already been disassociated,
318             // for example if the connection idle timeouts: this will fail
319             // the response, but the request may still be under processing.
320             // Eventually the request will also fail as the connection is closed
321             // and will arrive here without an exchange being present.
322             // We just ignore this fact, as the exchange has already been processed
323         }
324         else
325         {
326             throw new IllegalStateException();
327         }
328     }
329 
330     public boolean abort(Throwable cause)
331     {
332         // We want the return value to be that of the response
333         // because if the response has already successfully
334         // arrived then we failed to abort the exchange
335         sender.abort(cause);
336         return receiver.abort(cause);
337     }
338 
339     public void proceed(boolean proceed)
340     {
341         sender.proceed(proceed);
342     }
343 
344     @Override
345     public void close()
346     {
347         destination.remove(this);
348         getEndPoint().shutdownOutput();
349         LOG.debug("{} oshut", this);
350         getEndPoint().close();
351         LOG.debug("{} closed", this);
352     }
353 
354     @Override
355     public String toString()
356     {
357         return String.format("%s@%x(l:%s <-> r:%s)",
358                 HttpConnection.class.getSimpleName(),
359                 hashCode(),
360                 getEndPoint().getLocalAddress(),
361                 getEndPoint().getRemoteAddress());
362     }
363 }