1/*
2 * Copyright (C) 2007 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 java.util.Locale;
20
21/**
22 * HttpAuthHeader: a class to store HTTP authentication-header parameters.
23 * For more information, see: RFC 2617: HTTP Authentication.
24 *
25 * {@hide}
26 */
27public class HttpAuthHeader {
28    /**
29     * Possible HTTP-authentication header tokens to search for:
30     */
31    public final static String BASIC_TOKEN = "Basic";
32    public final static String DIGEST_TOKEN = "Digest";
33
34    private final static String REALM_TOKEN = "realm";
35    private final static String NONCE_TOKEN = "nonce";
36    private final static String STALE_TOKEN = "stale";
37    private final static String OPAQUE_TOKEN = "opaque";
38    private final static String QOP_TOKEN = "qop";
39    private final static String ALGORITHM_TOKEN = "algorithm";
40
41    /**
42     * An authentication scheme. We currently support two different schemes:
43     * HttpAuthHeader.BASIC  - basic, and
44     * HttpAuthHeader.DIGEST - digest (algorithm=MD5, QOP="auth").
45     */
46    private int mScheme;
47
48    public static final int UNKNOWN = 0;
49    public static final int BASIC = 1;
50    public static final int DIGEST = 2;
51
52    /**
53     * A flag, indicating that the previous request from the client was
54     * rejected because the nonce value was stale. If stale is TRUE
55     * (case-insensitive), the client may wish to simply retry the request
56     * with a new encrypted response, without reprompting the user for a
57     * new username and password.
58     */
59    private boolean mStale;
60
61    /**
62     * A string to be displayed to users so they know which username and
63     * password to use.
64     */
65    private String mRealm;
66
67    /**
68     * A server-specified data string which should be uniquely generated
69     * each time a 401 response is made.
70     */
71    private String mNonce;
72
73    /**
74     * A string of data, specified by the server, which should be returned
75     *  by the client unchanged in the Authorization header of subsequent
76     * requests with URIs in the same protection space.
77     */
78    private String mOpaque;
79
80    /**
81     * This directive is optional, but is made so only for backward
82     * compatibility with RFC 2069 [6]; it SHOULD be used by all
83     * implementations compliant with this version of the Digest scheme.
84     * If present, it is a quoted string of one or more tokens indicating
85     * the "quality of protection" values supported by the server.  The
86     * value "auth" indicates authentication; the value "auth-int"
87     * indicates authentication with integrity protection.
88     */
89    private String mQop;
90
91    /**
92     * A string indicating a pair of algorithms used to produce the digest
93     * and a checksum. If this is not present it is assumed to be "MD5".
94     */
95    private String mAlgorithm;
96
97    /**
98     * Is this authentication request a proxy authentication request?
99     */
100    private boolean mIsProxy;
101
102    /**
103     * Username string we get from the user.
104     */
105    private String mUsername;
106
107    /**
108     * Password string we get from the user.
109     */
110    private String mPassword;
111
112    /**
113     * Creates a new HTTP-authentication header object from the
114     * input header string.
115     * The header string is assumed to contain parameters of at
116     * most one authentication-scheme (ensured by the caller).
117     */
118    public HttpAuthHeader(String header) {
119        if (header != null) {
120            parseHeader(header);
121        }
122    }
123
124    /**
125     * @return True iff this is a proxy authentication header.
126     */
127    public boolean isProxy() {
128        return mIsProxy;
129    }
130
131    /**
132     * Marks this header as a proxy authentication header.
133     */
134    public void setProxy() {
135        mIsProxy = true;
136    }
137
138    /**
139     * @return The username string.
140     */
141    public String getUsername() {
142        return mUsername;
143    }
144
145    /**
146     * Sets the username string.
147     */
148    public void setUsername(String username) {
149        mUsername = username;
150    }
151
152    /**
153     * @return The password string.
154     */
155    public String getPassword() {
156        return mPassword;
157    }
158
159    /**
160     * Sets the password string.
161     */
162    public void setPassword(String password) {
163        mPassword = password;
164    }
165
166    /**
167     * @return True iff this is the  BASIC-authentication request.
168     */
169    public boolean isBasic () {
170        return mScheme == BASIC;
171    }
172
173    /**
174     * @return True iff this is the DIGEST-authentication request.
175     */
176    public boolean isDigest() {
177        return mScheme == DIGEST;
178    }
179
180    /**
181     * @return The authentication scheme requested. We currently
182     * support two schemes:
183     * HttpAuthHeader.BASIC  - basic, and
184     * HttpAuthHeader.DIGEST - digest (algorithm=MD5, QOP="auth").
185     */
186    public int getScheme() {
187        return mScheme;
188    }
189
190    /**
191     * @return True if indicating that the previous request from
192     * the client was rejected because the nonce value was stale.
193     */
194    public boolean getStale() {
195        return mStale;
196    }
197
198    /**
199     * @return The realm value or null if there is none.
200     */
201    public String getRealm() {
202        return mRealm;
203    }
204
205    /**
206     * @return The nonce value or null if there is none.
207     */
208    public String getNonce() {
209        return mNonce;
210    }
211
212    /**
213     * @return The opaque value or null if there is none.
214     */
215    public String getOpaque() {
216        return mOpaque;
217    }
218
219    /**
220     * @return The QOP ("quality-of_protection") value or null if
221     * there is none. The QOP value is always lower-case.
222     */
223    public String getQop() {
224        return mQop;
225    }
226
227    /**
228     * @return The name of the algorithm used or null if there is
229     * none. By default, MD5 is used.
230     */
231    public String getAlgorithm() {
232        return mAlgorithm;
233    }
234
235    /**
236     * @return True iff the authentication scheme requested by the
237     * server is supported; currently supported schemes:
238     * BASIC,
239     * DIGEST (only algorithm="md5", no qop or qop="auth).
240     */
241    public boolean isSupportedScheme() {
242        // it is a good idea to enforce non-null realms!
243        if (mRealm != null) {
244            if (mScheme == BASIC) {
245                return true;
246            } else {
247                if (mScheme == DIGEST) {
248                    return
249                        mAlgorithm.equals("md5") &&
250                        (mQop == null || mQop.equals("auth"));
251                }
252            }
253        }
254
255        return false;
256    }
257
258    /**
259     * Parses the header scheme name and then scheme parameters if
260     * the scheme is supported.
261     */
262    private void parseHeader(String header) {
263        if (HttpLog.LOGV) {
264            HttpLog.v("HttpAuthHeader.parseHeader(): header: " + header);
265        }
266
267        if (header != null) {
268            String parameters = parseScheme(header);
269            if (parameters != null) {
270                // if we have a supported scheme
271                if (mScheme != UNKNOWN) {
272                    parseParameters(parameters);
273                }
274            }
275        }
276    }
277
278    /**
279     * Parses the authentication scheme name. If we have a Digest
280     * scheme, sets the algorithm value to the default of MD5.
281     * @return The authentication scheme parameters string to be
282     * parsed later (if the scheme is supported) or null if failed
283     * to parse the scheme (the header value is null?).
284     */
285    private String parseScheme(String header) {
286        if (header != null) {
287            int i = header.indexOf(' ');
288            if (i >= 0) {
289                String scheme = header.substring(0, i).trim();
290                if (scheme.equalsIgnoreCase(DIGEST_TOKEN)) {
291                    mScheme = DIGEST;
292
293                    // md5 is the default algorithm!!!
294                    mAlgorithm = "md5";
295                } else {
296                    if (scheme.equalsIgnoreCase(BASIC_TOKEN)) {
297                        mScheme = BASIC;
298                    }
299                }
300
301                return header.substring(i + 1);
302            }
303        }
304
305        return null;
306    }
307
308    /**
309     * Parses a comma-separated list of authentification scheme
310     * parameters.
311     */
312    private void parseParameters(String parameters) {
313        if (HttpLog.LOGV) {
314            HttpLog.v("HttpAuthHeader.parseParameters():" +
315                      " parameters: " + parameters);
316        }
317
318        if (parameters != null) {
319            int i;
320            do {
321                i = parameters.indexOf(',');
322                if (i < 0) {
323                    // have only one parameter
324                    parseParameter(parameters);
325                } else {
326                    parseParameter(parameters.substring(0, i));
327                    parameters = parameters.substring(i + 1);
328                }
329            } while (i >= 0);
330        }
331    }
332
333    /**
334     * Parses a single authentication scheme parameter. The parameter
335     * string is expected to follow the format: PARAMETER=VALUE.
336     */
337    private void parseParameter(String parameter) {
338        if (parameter != null) {
339            // here, we are looking for the 1st occurence of '=' only!!!
340            int i = parameter.indexOf('=');
341            if (i >= 0) {
342                String token = parameter.substring(0, i).trim();
343                String value =
344                    trimDoubleQuotesIfAny(parameter.substring(i + 1).trim());
345
346                if (HttpLog.LOGV) {
347                    HttpLog.v("HttpAuthHeader.parseParameter():" +
348                              " token: " + token +
349                              " value: " + value);
350                }
351
352                if (token.equalsIgnoreCase(REALM_TOKEN)) {
353                    mRealm = value;
354                } else {
355                    if (mScheme == DIGEST) {
356                        parseParameter(token, value);
357                    }
358                }
359            }
360        }
361    }
362
363    /**
364     * If the token is a known parameter name, parses and initializes
365     * the token value.
366     */
367    private void parseParameter(String token, String value) {
368        if (token != null && value != null) {
369            if (token.equalsIgnoreCase(NONCE_TOKEN)) {
370                mNonce = value;
371                return;
372            }
373
374            if (token.equalsIgnoreCase(STALE_TOKEN)) {
375                parseStale(value);
376                return;
377            }
378
379            if (token.equalsIgnoreCase(OPAQUE_TOKEN)) {
380                mOpaque = value;
381                return;
382            }
383
384            if (token.equalsIgnoreCase(QOP_TOKEN)) {
385                mQop = value.toLowerCase(Locale.ROOT);
386                return;
387            }
388
389            if (token.equalsIgnoreCase(ALGORITHM_TOKEN)) {
390                mAlgorithm = value.toLowerCase(Locale.ROOT);
391                return;
392            }
393        }
394    }
395
396    /**
397     * Parses and initializes the 'stale' paramer value. Any value
398     * different from case-insensitive "true" is considered "false".
399     */
400    private void parseStale(String value) {
401        if (value != null) {
402            if (value.equalsIgnoreCase("true")) {
403                mStale = true;
404            }
405        }
406    }
407
408    /**
409     * Trims double-quotes around a parameter value if there are any.
410     * @return The string value without the outermost pair of double-
411     * quotes or null if the original value is null.
412     */
413    static private String trimDoubleQuotesIfAny(String value) {
414        if (value != null) {
415            int len = value.length();
416            if (len > 2 &&
417                value.charAt(0) == '\"' && value.charAt(len - 1) == '\"') {
418                return value.substring(1, len - 1);
419            }
420        }
421
422        return value;
423    }
424}
425