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