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