/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.sun.jini.jeri.internal.http; import com.sun.jini.jeri.internal.runtime.BASE64Encoder; import java.net.Authenticator; import java.net.InetAddress; import java.net.PasswordAuthentication; import java.net.UnknownHostException; import java.security.AccessController; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Remote HTTP server version/authentication information. * * REMIND: need manage/null out password more strictly? * * @author Sun Microsystems, Inc. * */ class ServerInfo implements Cloneable { /** blank timestamp value */ static final long NO_TIMESTAMP = -1L; /** hexadecimal char conversion table */ private static final char[] hexChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; /** server host name */ final String host; /** server port */ final int port; /** HTTP major version */ int major = 1; /** HTTP minor version */ int minor = 0; /** authentication scheme, if any */ String authScheme; /** authentication realm */ String authRealm; /** authentication algorithm */ String authAlgorithm; /** authentication cookie */ String authOpaque; /** authentication challenge */ String authNonce; /** authentication username */ String authUser; /** authentication password */ String authPassword; /** time of last update */ long timestamp = NO_TIMESTAMP; /** * Creates new ServerInfo for server at given host/port. */ ServerInfo(String host, int port) { this.host = host; this.port = port; } /** * Sets authentication information based on contents of given challenge * string (which should be the value of either the "WWW-Authenticate" or * "Proxy-Authenticate" header fields). If given string is null or empty, * clears any previous authentication information. */ void setAuthInfo(String str) throws HttpParseException { if (str == null || str.length() == 0) { authScheme = null; authRealm = null; authAlgorithm = null; authOpaque = null; authNonce = null; authUser = null; authPassword = null; return; } LineParser lp = new LineParser(str); Map entries = lp.getEntries("Digest"); if (entries != null) { String realm = (String) entries.get("realm"); String nonce = (String) entries.get("nonce"); if (realm != null && nonce != null) { authScheme = "Digest"; authRealm = realm; authNonce = nonce; authAlgorithm = (String) entries.get("algorithm"); authOpaque = (String) entries.get("opaque"); if (!"true".equalsIgnoreCase((String) entries.get("stale"))) { authUser = null; authPassword = null; } return; } } if ((entries = lp.getEntries("Basic")) != null) { String realm = (String) entries.get("realm"); if (realm != null) { authScheme = "Basic"; authRealm = realm; authAlgorithm = null; authOpaque = null; authNonce = null; authUser = null; authPassword = null; return; } } // REMIND: no supported schemes found; clear auth info? } /** * Updates authentication information based on contents of given string * (which should be the value of either the "Authorization-Info" or * "Proxy-Authorization-Info" header fields). If given string is null or * empty, current authentication settings are left unchanged. */ void updateAuthInfo(String str) throws HttpParseException { if (str == null || str.length() == 0) { return; } if ("Digest".equals(authScheme)) { LineParser lp = new LineParser(str); Map entries = lp.getAllEntries(); String nextNonce = (String) entries.get("nextnonce"); if (nextNonce != null) { authNonce = nextNonce; } } } /** * Returns (possibly null) authorization string based on current * authentication information in conjunction with the given request * arguments. */ String getAuthString(String protocol, String method, String uri) { if (authScheme == null) { return null; } if (authUser == null) { PasswordAuthentication pa = getPassword(protocol); if (pa == null) { return null; } String user = pa.getUserName(); char[] password = pa.getPassword(); if (user == null || password == null) { return null; } authUser = user; authPassword = new String(password); } if (authScheme.equals("Basic")) { BASE64Encoder enc = new BASE64Encoder(); return "Basic " + enc.encode((authUser + ":" + authPassword).getBytes()); } else if (authScheme.equals("Digest")) { String digest; try { digest = computeDigest(method, uri); } catch (NoSuchAlgorithmException ex) { return null; } String response = "Digest " + "username=\"" + authUser + "\", " + "realm=\"" + authRealm + "\", " + "nonce=\"" + authNonce + "\", " + "uri=\"" + uri + "\", " + "response=\"" + digest + "\""; if (authOpaque != null) { response += ", opaque=\"" + authOpaque + "\""; } if (authAlgorithm != null) { response += ", algorithm=" + authAlgorithm; } return response; } else { throw new InternalError(); } } /** * Computes digest authentication response for request using the given * method and uri. Throws NoSuchAlgorithmException if server-specified * digest algorithm not supported. */ private String computeDigest(String method, String uri) throws NoSuchAlgorithmException { // REMIND: cache MessageDigest? MessageDigest md = MessageDigest.getInstance( (authAlgorithm != null) ? authAlgorithm : "MD5"); String hashA1 = encode(md, authUser + ":" + authRealm + ":" + authPassword); String hashA2 = encode(md, method + ":" + uri); return encode(md, hashA1 + ":" + authNonce + ":" + hashA2); } /** * Returns digest of the given string, represented as string of hexadecimal * digits. */ private String encode(MessageDigest md, String str) { md.reset(); byte[] digest = md.digest(str.getBytes()); StringBuffer sbuf = new StringBuffer(digest.length * 2); for (int i = 0; i < digest.length; i++) { sbuf.append(hexChars[(digest[i] >>> 4) & 0xF]); sbuf.append(hexChars[digest[i] & 0xF]); } return sbuf.toString(); } public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException ex) { throw new InternalError(); } } /** * Class for parsing multi-part HTTP header lines that may appear as the * values of the WWW-Authenticate, Proxy-Authenticate, Authorization-Info * or Proxy-Authorization-Info header lines. */ private static class LineParser { /* token types */ private static final int EOL = -1; private static final int WORD = 0; private static final int QUOTE = 1; private static final int COMMA = 2; private static final int EQUALS = 3; private final List entries = new ArrayList(); private final char[] ca; private int pos = 0; private String tokenString = null; /** * Attempts to parse the given line into a series of key/optional value * definitions. Throws an HttpParseException if the line contains * syntax errors. */ LineParser(String line) throws HttpParseException { ca = line.toCharArray(); int tok = nextToken(); while (tok != EOL) { String key; if (tok == COMMA) { tok = nextToken(); continue; } else if (tok == WORD) { key = tokenString; } else { throw new HttpParseException("illegal key"); } tok = nextToken(); if (tok == COMMA) { entries.add(new String[] { key, null }); tok = nextToken(); continue; } else if (tok != EQUALS) { entries.add(new String[] { key, null }); continue; } tok = nextToken(); if (tok != WORD && tok != QUOTE) { throw new HttpParseException("illegal value"); } entries.add(new String[] { key, tokenString }); tok = nextToken(); if (tok == COMMA) { tok = nextToken(); continue; } else if (tok != EOL) { throw new HttpParseException("illegal separator"); } } } /** * Returns code indicating next token in line. If token type is WORD, * tokenString is set to the word text; if returned type is QUOTE, * tokenString is set to the quoted string's contents. */ private int nextToken() throws HttpParseException { int mark; while (pos < ca.length && Character.isWhitespace(ca[pos])) { pos++; } if (pos >= ca.length) { return EOL; } switch (ca[pos]) { case ',': pos++; return COMMA; case '=': pos++; return EQUALS; case '\"': mark = ++pos; while (pos < ca.length && ca[pos] != '\"') { pos++; } if (pos >= ca.length) { throw new HttpParseException( "unterminated quote string"); } tokenString = new String(ca, mark, pos++ - mark); return QUOTE; default: mark = pos; while (pos < ca.length) { char c = ca[pos]; if (c == ',' || c == '=' || c == '\"' || Character.isWhitespace(c)) { break; } pos++; } tokenString = new String(ca, mark, pos - mark); return WORD; } } /** * Returns all key/value entries associated with the given * authorization scheme in the parsed line, or null if the given scheme * was not described in the parsed line. Authorization scheme * identifiers are those which appear without a subsequent '=' or ',' * character before the next word; key/value entries are associated * with the nearest preceding scheme identifier. All key strings are * converted to lower case. */ Map getEntries(String scheme) { Map map = null; String[][] ea = (String[][]) entries.toArray(new String[entries.size()][]); int i; for (i = 0; i < ea.length; i++) { if (ea[i][1] == null && scheme.equalsIgnoreCase(ea[i][0])) { map = new HashMap(); break; } } if (map != null) { for (i++; i < ea.length && ea[i][1] != null; i++) { map.put(ea[i][0].toLowerCase(), ea[i][1]); } } return map; } /** * Returns the key/value entries encountered in the parsed line. This * method should be used to obtain the parse results for * Authorization-Info and Proxy-Authorization-Info values, which do not * contain authorization scheme identifiers. All key strings are * converted to lower case. */ Map getAllEntries() { Map map = new HashMap(); String[][] ea = (String[][]) entries.toArray(new String[entries.size()][]); for (int i = 0; i < ea.length; i++) { map.put(ea[i][0].toLowerCase(), ea[i][1]); } return map; } } /** * Obtains PasswordAuthentication from the currently installed * Authenticator. */ private PasswordAuthentication getPassword(final String protocol) { return (PasswordAuthentication) AccessController.doPrivileged( new PrivilegedAction() { public Object run() { InetAddress addr = null; try { addr = InetAddress.getByName(host); } catch (UnknownHostException ex) { } return Authenticator.requestPasswordAuthentication( addr, port, protocol, authRealm, authScheme); } } ); } }