1 /*
2  * Copyright (C) 2008-2009 Marc Blank
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.exchange;
19 
20 import android.net.Uri;
21 
22 import com.android.emailcommon.utility.EmailClientConnectionManager;
23 
24 import org.apache.http.Header;
25 import org.apache.http.HttpEntity;
26 import org.apache.http.HttpResponse;
27 import org.apache.http.HttpStatus;
28 import org.apache.http.client.HttpClient;
29 import org.apache.http.client.methods.HttpUriRequest;
30 
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.util.zip.GZIPInputStream;
34 
35 /**
36  * Encapsulate a response to an HTTP POST
37  */
38 public class EasResponse {
39     // MSFT's custom HTTP result code indicating the need to provision
40     static private final int HTTP_NEED_PROVISIONING = 449;
41 
42     // Microsoft-defined HTTP response indicating a redirect to a "better" server.
43     // Why is this a 4xx instead of 3xx? Because EAS considers this a "Device misconfigured" error.
44     static private final int HTTP_REDIRECT = 451;
45 
46     private final HttpResponse mResponse;
47     private final HttpEntity mEntity;
48     private final int mLength;
49     private InputStream mInputStream;
50     private boolean mClosed;
51 
52     private final int mStatus;
53 
54     /**
55      * Whether or not a certificate was requested by the server and missing.
56      * If this is set, it is essentially a 403 whereby the failure was due
57      */
58     private final boolean mClientCertRequested;
59 
EasResponse(final HttpResponse response, final EmailClientConnectionManager connManager, final long reqTime)60     private EasResponse(final HttpResponse response,
61             final EmailClientConnectionManager connManager, final long reqTime) {
62         mResponse = response;
63         mEntity = (response == null) ? null : mResponse.getEntity();
64         if (mEntity !=  null) {
65             mLength = (int) mEntity.getContentLength();
66         } else {
67             mLength = 0;
68         }
69         int status = response.getStatusLine().getStatusCode();
70         mClientCertRequested =
71                 isAuthError(status) && connManager.hasDetectedUnsatisfiedCertReq(reqTime);
72         if (mClientCertRequested) {
73             status = HttpStatus.SC_UNAUTHORIZED;
74             mClosed = true;
75         }
76         mStatus = status;
77     }
78 
fromHttpRequest( EmailClientConnectionManager connManager, HttpClient client, HttpUriRequest request)79     public static EasResponse fromHttpRequest(
80             EmailClientConnectionManager connManager, HttpClient client, HttpUriRequest request)
81             throws IOException {
82         final long reqTime = System.currentTimeMillis();
83         final HttpResponse response = client.execute(request);
84         return new EasResponse(response, connManager, reqTime);
85     }
86 
isSuccess()87     public boolean isSuccess() {
88         return mStatus == HttpStatus.SC_OK;
89     }
90 
isForbidden()91     public boolean isForbidden() {
92         return mStatus == HttpStatus.SC_FORBIDDEN;
93     }
94 
95     /**
96      * @return Whether this response indicates an authentication error.
97      */
isAuthError()98     public boolean isAuthError() {
99         return mStatus == HttpStatus.SC_UNAUTHORIZED;
100     }
101 
102     /**
103      * @return Whether this response indicates a provisioning error.
104      */
isProvisionError()105     public boolean isProvisionError() {
106         return (mStatus == HTTP_NEED_PROVISIONING) || isForbidden();
107     }
108 
109     /**
110      * @return Whether this response indicates a redirect error.
111      */
isRedirectError()112     public boolean isRedirectError() {
113         return mStatus == HTTP_REDIRECT;
114     }
115 
116     /**
117      * Determine whether an HTTP code represents an authentication error
118      * @param code the HTTP code returned by the server
119      * @return whether or not the code represents an authentication error
120      */
isAuthError(int code)121     private static boolean isAuthError(int code) {
122         return (code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN);
123     }
124 
125     /**
126      * Read the redirect address from this response, if it's present.
127      * @return The new host address, or null if it's not there.
128      */
getRedirectAddress()129     public String getRedirectAddress() {
130         final Header locHeader = getHeader("X-MS-Location");
131         if (locHeader != null) {
132             return Uri.parse(locHeader.getValue()).getHost();
133         }
134         return null;
135     }
136 
137     /**
138      * Return an appropriate input stream for the response, either a GZIPInputStream, for
139      * compressed data, or a generic InputStream otherwise
140      * @return the input stream for the response
141      */
getInputStream()142     public InputStream getInputStream() {
143         if (mInputStream != null || mClosed) {
144             throw new IllegalStateException("Can't reuse stream or get closed stream");
145         } else if (mEntity == null) {
146             throw new IllegalStateException("Can't get input stream without entity");
147         }
148         InputStream is = null;
149         try {
150             // Get the default input stream for the entity
151             is = mEntity.getContent();
152             Header ceHeader = mResponse.getFirstHeader("Content-Encoding");
153             if (ceHeader != null) {
154                 String encoding = ceHeader.getValue();
155                 // If we're gzip encoded, wrap appropriately
156                 if (encoding.toLowerCase().equals("gzip")) {
157                     is = new GZIPInputStream(is);
158                 }
159             }
160         } catch (IllegalStateException e1) {
161         } catch (IOException e1) {
162         }
163         mInputStream = is;
164         return is;
165     }
166 
isEmpty()167     public boolean isEmpty() {
168         return mLength == 0;
169     }
170 
getStatus()171     public int getStatus() {
172         return mStatus;
173     }
174 
isMissingCertificate()175     public boolean isMissingCertificate() {
176         return mClientCertRequested;
177     }
178 
getHeader(String name)179     public Header getHeader(String name) {
180         return (mResponse == null) ? null : mResponse.getFirstHeader(name);
181     }
182 
getLength()183     public int getLength() {
184         return mLength;
185     }
186 
close()187     public void close() {
188         if (!mClosed) {
189             if (mEntity != null) {
190                 try {
191                     mEntity.consumeContent();
192                 } catch (IOException e) {
193                     // No harm, no foul
194                 }
195             }
196             if (mInputStream instanceof GZIPInputStream) {
197                 try {
198                     mInputStream.close();
199                 } catch (IOException e) {
200                     // We tried
201                 }
202             }
203             mClosed = true;
204         }
205     }
206 }