1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.net.http;
18 
19 import org.apache.http.HttpConnection;
20 import org.apache.http.HttpClientConnection;
21 import org.apache.http.HttpConnectionMetrics;
22 import org.apache.http.HttpEntity;
23 import org.apache.http.HttpEntityEnclosingRequest;
24 import org.apache.http.HttpException;
25 import org.apache.http.HttpInetConnection;
26 import org.apache.http.HttpRequest;
27 import org.apache.http.HttpResponse;
28 import org.apache.http.NoHttpResponseException;
29 import org.apache.http.StatusLine;
30 import org.apache.http.entity.BasicHttpEntity;
31 import org.apache.http.entity.ContentLengthStrategy;
32 import org.apache.http.impl.HttpConnectionMetricsImpl;
33 import org.apache.http.impl.entity.EntitySerializer;
34 import org.apache.http.impl.entity.StrictContentLengthStrategy;
35 import org.apache.http.impl.io.ChunkedInputStream;
36 import org.apache.http.impl.io.ContentLengthInputStream;
37 import org.apache.http.impl.io.HttpRequestWriter;
38 import org.apache.http.impl.io.IdentityInputStream;
39 import org.apache.http.impl.io.SocketInputBuffer;
40 import org.apache.http.impl.io.SocketOutputBuffer;
41 import org.apache.http.io.HttpMessageWriter;
42 import org.apache.http.io.SessionInputBuffer;
43 import org.apache.http.io.SessionOutputBuffer;
44 import org.apache.http.message.BasicLineParser;
45 import org.apache.http.message.ParserCursor;
46 import org.apache.http.params.CoreConnectionPNames;
47 import org.apache.http.params.HttpConnectionParams;
48 import org.apache.http.params.HttpParams;
49 import org.apache.http.ParseException;
50 import org.apache.http.util.CharArrayBuffer;
51 
52 import java.io.IOException;
53 import java.net.InetAddress;
54 import java.net.Socket;
55 import java.net.SocketException;
56 
57 /**
58  * A alternate class for (@link DefaultHttpClientConnection).
59  * It has better performance than DefaultHttpClientConnection
60  *
61  * {@hide}
62  */
63 public class AndroidHttpClientConnection
64         implements HttpInetConnection, HttpConnection {
65 
66     private SessionInputBuffer inbuffer = null;
67     private SessionOutputBuffer outbuffer = null;
68     private int maxHeaderCount;
69     // store CoreConnectionPNames.MAX_LINE_LENGTH for performance
70     private int maxLineLength;
71 
72     private final EntitySerializer entityserializer;
73 
74     private HttpMessageWriter requestWriter = null;
75     private HttpConnectionMetricsImpl metrics = null;
76     private volatile boolean open;
77     private Socket socket = null;
78 
AndroidHttpClientConnection()79     public AndroidHttpClientConnection() {
80         this.entityserializer =  new EntitySerializer(
81                 new StrictContentLengthStrategy());
82     }
83 
84     /**
85      * Bind socket and set HttpParams to AndroidHttpClientConnection
86      * @param socket outgoing socket
87      * @param params HttpParams
88      * @throws IOException
89       */
bind( final Socket socket, final HttpParams params)90     public void bind(
91             final Socket socket,
92             final HttpParams params) throws IOException {
93         if (socket == null) {
94             throw new IllegalArgumentException("Socket may not be null");
95         }
96         if (params == null) {
97             throw new IllegalArgumentException("HTTP parameters may not be null");
98         }
99         assertNotOpen();
100         socket.setTcpNoDelay(HttpConnectionParams.getTcpNoDelay(params));
101         socket.setSoTimeout(HttpConnectionParams.getSoTimeout(params));
102 
103         int linger = HttpConnectionParams.getLinger(params);
104         if (linger >= 0) {
105             socket.setSoLinger(linger > 0, linger);
106         }
107         this.socket = socket;
108 
109         int buffersize = HttpConnectionParams.getSocketBufferSize(params);
110         this.inbuffer = new SocketInputBuffer(socket, buffersize, params);
111         this.outbuffer = new SocketOutputBuffer(socket, buffersize, params);
112 
113         maxHeaderCount = params.getIntParameter(
114                 CoreConnectionPNames.MAX_HEADER_COUNT, -1);
115         maxLineLength = params.getIntParameter(
116                 CoreConnectionPNames.MAX_LINE_LENGTH, -1);
117 
118         this.requestWriter = new HttpRequestWriter(outbuffer, null, params);
119 
120         this.metrics = new HttpConnectionMetricsImpl(
121                 inbuffer.getMetrics(),
122                 outbuffer.getMetrics());
123 
124         this.open = true;
125     }
126 
127     @Override
toString()128     public String toString() {
129         StringBuilder buffer = new StringBuilder();
130         buffer.append(getClass().getSimpleName()).append("[");
131         if (isOpen()) {
132             buffer.append(getRemotePort());
133         } else {
134             buffer.append("closed");
135         }
136         buffer.append("]");
137         return buffer.toString();
138     }
139 
140 
assertNotOpen()141     private void assertNotOpen() {
142         if (this.open) {
143             throw new IllegalStateException("Connection is already open");
144         }
145     }
146 
assertOpen()147     private void assertOpen() {
148         if (!this.open) {
149             throw new IllegalStateException("Connection is not open");
150         }
151     }
152 
isOpen()153     public boolean isOpen() {
154         // to make this method useful, we want to check if the socket is connected
155         return (this.open && this.socket != null && this.socket.isConnected());
156     }
157 
getLocalAddress()158     public InetAddress getLocalAddress() {
159         if (this.socket != null) {
160             return this.socket.getLocalAddress();
161         } else {
162             return null;
163         }
164     }
165 
getLocalPort()166     public int getLocalPort() {
167         if (this.socket != null) {
168             return this.socket.getLocalPort();
169         } else {
170             return -1;
171         }
172     }
173 
getRemoteAddress()174     public InetAddress getRemoteAddress() {
175         if (this.socket != null) {
176             return this.socket.getInetAddress();
177         } else {
178             return null;
179         }
180     }
181 
getRemotePort()182     public int getRemotePort() {
183         if (this.socket != null) {
184             return this.socket.getPort();
185         } else {
186             return -1;
187         }
188     }
189 
setSocketTimeout(int timeout)190     public void setSocketTimeout(int timeout) {
191         assertOpen();
192         if (this.socket != null) {
193             try {
194                 this.socket.setSoTimeout(timeout);
195             } catch (SocketException ignore) {
196                 // It is not quite clear from the original documentation if there are any
197                 // other legitimate cases for a socket exception to be thrown when setting
198                 // SO_TIMEOUT besides the socket being already closed
199             }
200         }
201     }
202 
getSocketTimeout()203     public int getSocketTimeout() {
204         if (this.socket != null) {
205             try {
206                 return this.socket.getSoTimeout();
207             } catch (SocketException ignore) {
208                 return -1;
209             }
210         } else {
211             return -1;
212         }
213     }
214 
shutdown()215     public void shutdown() throws IOException {
216         this.open = false;
217         Socket tmpsocket = this.socket;
218         if (tmpsocket != null) {
219             tmpsocket.close();
220         }
221     }
222 
close()223     public void close() throws IOException {
224         if (!this.open) {
225             return;
226         }
227         this.open = false;
228         doFlush();
229         try {
230             try {
231                 this.socket.shutdownOutput();
232             } catch (IOException ignore) {
233             }
234             try {
235                 this.socket.shutdownInput();
236             } catch (IOException ignore) {
237             }
238         } catch (UnsupportedOperationException ignore) {
239             // if one isn't supported, the other one isn't either
240         }
241         this.socket.close();
242     }
243 
244     /**
245      * Sends the request line and all headers over the connection.
246      * @param request the request whose headers to send.
247      * @throws HttpException
248      * @throws IOException
249      */
sendRequestHeader(final HttpRequest request)250     public void sendRequestHeader(final HttpRequest request)
251             throws HttpException, IOException {
252         if (request == null) {
253             throw new IllegalArgumentException("HTTP request may not be null");
254         }
255         assertOpen();
256         this.requestWriter.write(request);
257         this.metrics.incrementRequestCount();
258     }
259 
260     /**
261      * Sends the request entity over the connection.
262      * @param request the request whose entity to send.
263      * @throws HttpException
264      * @throws IOException
265      */
sendRequestEntity(final HttpEntityEnclosingRequest request)266     public void sendRequestEntity(final HttpEntityEnclosingRequest request)
267             throws HttpException, IOException {
268         if (request == null) {
269             throw new IllegalArgumentException("HTTP request may not be null");
270         }
271         assertOpen();
272         if (request.getEntity() == null) {
273             return;
274         }
275         this.entityserializer.serialize(
276                 this.outbuffer,
277                 request,
278                 request.getEntity());
279     }
280 
doFlush()281     protected void doFlush() throws IOException {
282         this.outbuffer.flush();
283     }
284 
flush()285     public void flush() throws IOException {
286         assertOpen();
287         doFlush();
288     }
289 
290     /**
291      * Parses the response headers and adds them to the
292      * given {@code headers} object, and returns the response StatusLine
293      * @param headers store parsed header to headers.
294      * @throws IOException
295      * @return StatusLine
296      * @see HttpClientConnection#receiveResponseHeader()
297       */
parseResponseHeader(Headers headers)298     public StatusLine parseResponseHeader(Headers headers)
299             throws IOException, ParseException {
300         assertOpen();
301 
302         CharArrayBuffer current = new CharArrayBuffer(64);
303 
304         if (inbuffer.readLine(current) == -1) {
305             throw new NoHttpResponseException("The target server failed to respond");
306         }
307 
308         // Create the status line from the status string
309         StatusLine statusline = BasicLineParser.DEFAULT.parseStatusLine(
310                 current, new ParserCursor(0, current.length()));
311 
312         if (HttpLog.LOGV) HttpLog.v("read: " + statusline);
313         int statusCode = statusline.getStatusCode();
314 
315         // Parse header body
316         CharArrayBuffer previous = null;
317         int headerNumber = 0;
318         while(true) {
319             if (current == null) {
320                 current = new CharArrayBuffer(64);
321             } else {
322                 // This must be he buffer used to parse the status
323                 current.clear();
324             }
325             int l = inbuffer.readLine(current);
326             if (l == -1 || current.length() < 1) {
327                 break;
328             }
329             // Parse the header name and value
330             // Check for folded headers first
331             // Detect LWS-char see HTTP/1.0 or HTTP/1.1 Section 2.2
332             // discussion on folded headers
333             char first = current.charAt(0);
334             if ((first == ' ' || first == '\t') && previous != null) {
335                 // we have continuation folded header
336                 // so append value
337                 int start = 0;
338                 int length = current.length();
339                 while (start < length) {
340                     char ch = current.charAt(start);
341                     if (ch != ' ' && ch != '\t') {
342                         break;
343                     }
344                     start++;
345                 }
346                 if (maxLineLength > 0 &&
347                         previous.length() + 1 + current.length() - start >
348                             maxLineLength) {
349                     throw new IOException("Maximum line length limit exceeded");
350                 }
351                 previous.append(' ');
352                 previous.append(current, start, current.length() - start);
353             } else {
354                 if (previous != null) {
355                     headers.parseHeader(previous);
356                 }
357                 headerNumber++;
358                 previous = current;
359                 current = null;
360             }
361             if (maxHeaderCount > 0 && headerNumber >= maxHeaderCount) {
362                 throw new IOException("Maximum header count exceeded");
363             }
364         }
365 
366         if (previous != null) {
367             headers.parseHeader(previous);
368         }
369 
370         if (statusCode >= 200) {
371             this.metrics.incrementResponseCount();
372         }
373         return statusline;
374     }
375 
376     /**
377      * Return the next response entity.
378      * @param headers contains values for parsing entity
379      * @see HttpClientConnection#receiveResponseEntity(HttpResponse response)
380      */
receiveResponseEntity(final Headers headers)381     public HttpEntity receiveResponseEntity(final Headers headers) {
382         assertOpen();
383         BasicHttpEntity entity = new BasicHttpEntity();
384 
385         long len = determineLength(headers);
386         if (len == ContentLengthStrategy.CHUNKED) {
387             entity.setChunked(true);
388             entity.setContentLength(-1);
389             entity.setContent(new ChunkedInputStream(inbuffer));
390         } else if (len == ContentLengthStrategy.IDENTITY) {
391             entity.setChunked(false);
392             entity.setContentLength(-1);
393             entity.setContent(new IdentityInputStream(inbuffer));
394         } else {
395             entity.setChunked(false);
396             entity.setContentLength(len);
397             entity.setContent(new ContentLengthInputStream(inbuffer, len));
398         }
399 
400         String contentTypeHeader = headers.getContentType();
401         if (contentTypeHeader != null) {
402             entity.setContentType(contentTypeHeader);
403         }
404         String contentEncodingHeader = headers.getContentEncoding();
405         if (contentEncodingHeader != null) {
406             entity.setContentEncoding(contentEncodingHeader);
407         }
408 
409        return entity;
410     }
411 
determineLength(final Headers headers)412     private long determineLength(final Headers headers) {
413         long transferEncoding = headers.getTransferEncoding();
414         // We use Transfer-Encoding if present and ignore Content-Length.
415         // RFC2616, 4.4 item number 3
416         if (transferEncoding < Headers.NO_TRANSFER_ENCODING) {
417             return transferEncoding;
418         } else {
419             long contentlen = headers.getContentLength();
420             if (contentlen > Headers.NO_CONTENT_LENGTH) {
421                 return contentlen;
422             } else {
423                 return ContentLengthStrategy.IDENTITY;
424             }
425         }
426     }
427 
428     /**
429      * Checks whether this connection has gone down.
430      * Network connections may get closed during some time of inactivity
431      * for several reasons. The next time a read is attempted on such a
432      * connection it will throw an IOException.
433      * This method tries to alleviate this inconvenience by trying to
434      * find out if a connection is still usable. Implementations may do
435      * that by attempting a read with a very small timeout. Thus this
436      * method may block for a small amount of time before returning a result.
437      * It is therefore an <i>expensive</i> operation.
438      *
439      * @return  <code>true</code> if attempts to use this connection are
440      *          likely to succeed, or <code>false</code> if they are likely
441      *          to fail and this connection should be closed
442      */
isStale()443     public boolean isStale() {
444         assertOpen();
445         try {
446             this.inbuffer.isDataAvailable(1);
447             return false;
448         } catch (IOException ex) {
449             return true;
450         }
451     }
452 
453     /**
454      * Returns a collection of connection metrcis
455      * @return HttpConnectionMetrics
456      */
getMetrics()457     public HttpConnectionMetrics getMetrics() {
458         return this.metrics;
459     }
460 }
461