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