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
17package android.net.http;
18
19import org.apache.http.HttpConnection;
20import org.apache.http.HttpClientConnection;
21import org.apache.http.HttpConnectionMetrics;
22import org.apache.http.HttpEntity;
23import org.apache.http.HttpEntityEnclosingRequest;
24import org.apache.http.HttpException;
25import org.apache.http.HttpInetConnection;
26import org.apache.http.HttpRequest;
27import org.apache.http.HttpResponse;
28import org.apache.http.NoHttpResponseException;
29import org.apache.http.StatusLine;
30import org.apache.http.entity.BasicHttpEntity;
31import org.apache.http.entity.ContentLengthStrategy;
32import org.apache.http.impl.HttpConnectionMetricsImpl;
33import org.apache.http.impl.entity.EntitySerializer;
34import org.apache.http.impl.entity.StrictContentLengthStrategy;
35import org.apache.http.impl.io.ChunkedInputStream;
36import org.apache.http.impl.io.ContentLengthInputStream;
37import org.apache.http.impl.io.HttpRequestWriter;
38import org.apache.http.impl.io.IdentityInputStream;
39import org.apache.http.impl.io.SocketInputBuffer;
40import org.apache.http.impl.io.SocketOutputBuffer;
41import org.apache.http.io.HttpMessageWriter;
42import org.apache.http.io.SessionInputBuffer;
43import org.apache.http.io.SessionOutputBuffer;
44import org.apache.http.message.BasicLineParser;
45import org.apache.http.message.ParserCursor;
46import org.apache.http.params.CoreConnectionPNames;
47import org.apache.http.params.HttpConnectionParams;
48import org.apache.http.params.HttpParams;
49import org.apache.http.ParseException;
50import org.apache.http.util.CharArrayBuffer;
51
52import java.io.IOException;
53import java.net.InetAddress;
54import java.net.Socket;
55import java.net.SocketException;
56
57/**
58 * A alternate class for (@link DefaultHttpClientConnection).
59 * It has better performance than DefaultHttpClientConnection
60 *
61 * {@hide}
62 */
63public 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
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      */
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
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
141    private void assertNotOpen() {
142        if (this.open) {
143            throw new IllegalStateException("Connection is already open");
144        }
145    }
146
147    private void assertOpen() {
148        if (!this.open) {
149            throw new IllegalStateException("Connection is not open");
150        }
151    }
152
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
158    public InetAddress getLocalAddress() {
159        if (this.socket != null) {
160            return this.socket.getLocalAddress();
161        } else {
162            return null;
163        }
164    }
165
166    public int getLocalPort() {
167        if (this.socket != null) {
168            return this.socket.getLocalPort();
169        } else {
170            return -1;
171        }
172    }
173
174    public InetAddress getRemoteAddress() {
175        if (this.socket != null) {
176            return this.socket.getInetAddress();
177        } else {
178            return null;
179        }
180    }
181
182    public int getRemotePort() {
183        if (this.socket != null) {
184            return this.socket.getPort();
185        } else {
186            return -1;
187        }
188    }
189
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
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
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
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     */
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     */
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
281    protected void doFlush() throws IOException {
282        this.outbuffer.flush();
283    }
284
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      */
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     */
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
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     */
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     */
457    public HttpConnectionMetrics getMetrics() {
458        return this.metrics;
459    }
460}
461