1/* 2 * Copyright (C) 2012 Square, Inc. 3 * Copyright (C) 2011 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 */ 17package com.squareup.okhttp.internal.http; 18 19import com.squareup.okhttp.internal.Base64; 20import java.io.IOException; 21import java.net.Authenticator; 22import java.net.InetAddress; 23import java.net.InetSocketAddress; 24import java.net.PasswordAuthentication; 25import java.net.Proxy; 26import java.net.URL; 27import java.util.ArrayList; 28import java.util.List; 29 30import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; 31import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; 32 33/** Handles HTTP authentication headers from origin and proxy servers. */ 34public final class HttpAuthenticator { 35 private HttpAuthenticator() { 36 } 37 38 /** 39 * React to a failed authorization response by looking up new credentials. 40 * 41 * @return true if credentials have been added to successorRequestHeaders 42 * and another request should be attempted. 43 */ 44 public static boolean processAuthHeader(int responseCode, RawHeaders responseHeaders, 45 RawHeaders successorRequestHeaders, Proxy proxy, URL url) throws IOException { 46 if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) { 47 throw new IllegalArgumentException(); 48 } 49 50 // Keep asking for username/password until authorized. 51 String challengeHeader = 52 responseCode == HTTP_PROXY_AUTH ? "Proxy-Authenticate" : "WWW-Authenticate"; 53 String credentials = getCredentials(responseHeaders, challengeHeader, proxy, url); 54 if (credentials == null) { 55 return false; // Could not find credentials so end the request cycle. 56 } 57 58 // Add authorization credentials, bypassing the already-connected check. 59 String fieldName = responseCode == HTTP_PROXY_AUTH ? "Proxy-Authorization" : "Authorization"; 60 successorRequestHeaders.set(fieldName, credentials); 61 return true; 62 } 63 64 /** 65 * Returns the authorization credentials that may satisfy the challenge. 66 * Returns null if a challenge header was not provided or if credentials 67 * were not available. 68 */ 69 private static String getCredentials(RawHeaders responseHeaders, String challengeHeader, 70 Proxy proxy, URL url) throws IOException { 71 List<Challenge> challenges = parseChallenges(responseHeaders, challengeHeader); 72 if (challenges.isEmpty()) { 73 return null; 74 } 75 76 for (Challenge challenge : challenges) { 77 // Use the global authenticator to get the password. 78 PasswordAuthentication auth; 79 if (responseHeaders.getResponseCode() == HTTP_PROXY_AUTH) { 80 InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address(); 81 auth = Authenticator.requestPasswordAuthentication(proxyAddress.getHostName(), 82 getConnectToInetAddress(proxy, url), proxyAddress.getPort(), url.getProtocol(), 83 challenge.realm, challenge.scheme, url, Authenticator.RequestorType.PROXY); 84 } else { 85 auth = Authenticator.requestPasswordAuthentication(url.getHost(), 86 getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(), challenge.realm, 87 challenge.scheme, url, Authenticator.RequestorType.SERVER); 88 } 89 if (auth == null) { 90 continue; 91 } 92 93 // Use base64 to encode the username and password. 94 String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword()); 95 byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1"); 96 String encoded = Base64.encode(bytes); 97 return challenge.scheme + " " + encoded; 98 } 99 100 return null; 101 } 102 103 private static InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException { 104 return (proxy != null && proxy.type() != Proxy.Type.DIRECT) 105 ? ((InetSocketAddress) proxy.address()).getAddress() : InetAddress.getByName(url.getHost()); 106 } 107 108 /** 109 * Parse RFC 2617 challenges. This API is only interested in the scheme 110 * name and realm. 111 */ 112 private static List<Challenge> parseChallenges(RawHeaders responseHeaders, 113 String challengeHeader) { 114 // auth-scheme = token 115 // auth-param = token "=" ( token | quoted-string ) 116 // challenge = auth-scheme 1*SP 1#auth-param 117 // realm = "realm" "=" realm-value 118 // realm-value = quoted-string 119 List<Challenge> result = new ArrayList<Challenge>(); 120 for (int h = 0; h < responseHeaders.length(); h++) { 121 if (!challengeHeader.equalsIgnoreCase(responseHeaders.getFieldName(h))) { 122 continue; 123 } 124 String value = responseHeaders.getValue(h); 125 int pos = 0; 126 while (pos < value.length()) { 127 int tokenStart = pos; 128 pos = HeaderParser.skipUntil(value, pos, " "); 129 130 String scheme = value.substring(tokenStart, pos).trim(); 131 pos = HeaderParser.skipWhitespace(value, pos); 132 133 // TODO: This currently only handles schemes with a 'realm' parameter; 134 // It needs to be fixed to handle any scheme and any parameters 135 // http://code.google.com/p/android/issues/detail?id=11140 136 137 if (!value.regionMatches(pos, "realm=\"", 0, "realm=\"".length())) { 138 break; // Unexpected challenge parameter; give up! 139 } 140 141 pos += "realm=\"".length(); 142 int realmStart = pos; 143 pos = HeaderParser.skipUntil(value, pos, "\""); 144 String realm = value.substring(realmStart, pos); 145 pos++; // Consume '"' close quote. 146 pos = HeaderParser.skipUntil(value, pos, ","); 147 pos++; // Consume ',' comma. 148 pos = HeaderParser.skipWhitespace(value, pos); 149 result.add(new Challenge(scheme, realm)); 150 } 151 } 152 return result; 153 } 154 155 /** An RFC 2617 challenge. */ 156 private static final class Challenge { 157 final String scheme; 158 final String realm; 159 160 Challenge(String scheme, String realm) { 161 this.scheme = scheme; 162 this.realm = realm; 163 } 164 165 @Override public boolean equals(Object o) { 166 return o instanceof Challenge 167 && ((Challenge) o).scheme.equals(scheme) 168 && ((Challenge) o).realm.equals(realm); 169 } 170 171 @Override public int hashCode() { 172 return scheme.hashCode() + 31 * realm.hashCode(); 173 } 174 } 175} 176