/* * ==================================================================== * Copyright (c) 2004-2012 TMate Software Ltd. All rights reserved. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms * are also available at http://svnkit.com/license.html. * If newer versions of this license are posted there, you may use a * newer version instead, at your option. * ==================================================================== */ package org.tmatesoft.svn.core.internal.io.svn.sasl; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.sasl.RealmCallback; import javax.security.sasl.Sasl; import javax.security.sasl.SaslClient; import javax.security.sasl.SaslClientFactory; import javax.security.sasl.SaslException; import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNErrorMessage; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.auth.BasicAuthenticationManager; import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; import org.tmatesoft.svn.core.auth.SVNAuthentication; import org.tmatesoft.svn.core.auth.SVNPasswordAuthentication; import org.tmatesoft.svn.core.internal.io.svn.SVNAuthenticator; import org.tmatesoft.svn.core.internal.io.svn.SVNConnection; import org.tmatesoft.svn.core.internal.io.svn.SVNPlainAuthenticator; import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryImpl; import org.tmatesoft.svn.core.internal.util.SVNBase64; import org.tmatesoft.svn.core.internal.util.SVNHashMap; import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; import org.tmatesoft.svn.util.SVNDebugLog; import org.tmatesoft.svn.util.SVNLogType; /** * @version 1.3 * @author TMate Software Ltd. */ public class SVNSaslAuthenticator extends SVNAuthenticator { private SaslClient myClient; private ISVNAuthenticationManager myAuthenticationManager; private SVNAuthentication myAuthentication; public SVNSaslAuthenticator(SVNConnection connection) throws SVNException { super(connection); } public SVNAuthentication authenticate(List mechs, String realm, SVNRepositoryImpl repository) throws SVNException { boolean failed = true; setLastError(null); myAuthenticationManager = repository.getAuthenticationManager(); myAuthentication = null; boolean isAnonymous = false; if (mechs.contains("EXTERNAL") && repository.getExternalUserName() != null) { mechs = new ArrayList(); mechs.add("EXTERNAL"); } else { for (Iterator mech = mechs.iterator(); mech.hasNext();) { String m = (String) mech.next(); if ("ANONYMOUS".equals(m) || "EXTERNAL".equals(m) || "PLAIN".equals(m)) { mechs = new ArrayList(); isAnonymous = "ANONYMOUS".equals(m); mechs.add(m); break; } } } dispose(); try { myClient = createSaslClient(mechs, realm, repository, repository.getLocation()); while(true) { if (myClient == null) { return new SVNPlainAuthenticator(getConnection()).authenticate(mechs, realm, repository); } // reiterate from the first available credentials next time: boolean startOver = false; try { if (tryAuthentication(repository, getMechanismName(myClient, isAnonymous))) { if (myAuthenticationManager != null && myAuthentication != null) { String realmName = getFullRealmName(repository.getLocation(), realm); BasicAuthenticationManager.acknowledgeAuthentication(true, myAuthentication.getKind(), realmName, null, myAuthentication, repository.getLocation(), myAuthenticationManager); } failed = false; setLastError(null); setEncryption(repository); break; } // some sort of authentication error. } catch (SaslException e) { // it may be plain replaced with anonymous. String mechName = getMechanismName(myClient, isAnonymous); mechs.remove(mechName); startOver = true; } if (myAuthenticationManager != null) { SVNErrorMessage error = getLastError(); if (error == null) { error = SVNErrorMessage.create(SVNErrorCode.RA_NOT_AUTHORIZED); setLastError(error); } if (myAuthentication != null) { String realmName = getFullRealmName(repository.getLocation(), realm); BasicAuthenticationManager.acknowledgeAuthentication(false, myAuthentication.getKind(), realmName, getLastError(), myAuthentication, repository.getLocation(), myAuthenticationManager); } else { // automatically generated authentication, do not try this mech again, will lead to the same error. // mechs.remove(getMechanismName(myClient, isAnonymous)); } } dispose(); if (mechs.isEmpty()) { failed = true; break; } if (startOver) { myAuthentication = null; } myClient = createSaslClient(mechs, realm, repository, repository.getLocation()); } } finally { if (failed) { dispose(); } } if (getLastError() != null) { SVNErrorManager.error(getLastError(), SVNLogType.NETWORK); } return myAuthentication; } public void dispose() { if (myClient != null) { try { myClient.dispose(); } catch (SaslException e) { // } } } protected boolean tryAuthentication(SVNRepositoryImpl repos, String mechName) throws SaslException, SVNException { String initialChallenge = null; boolean expectChallenge = !("ANONYMOUS".equals(mechName) || "EXTERNAL".equals(mechName) || "PLAIN".equals(mechName)); if ("EXTERNAL".equals(mechName) && repos.getExternalUserName() != null) { initialChallenge = ""; } else if (myClient.hasInitialResponse()) { // compute initial response byte[] initialResponse = null; initialResponse = myClient.evaluateChallenge(new byte[0]); if (initialResponse == null) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_NOT_AUTHORIZED, "Unexpected initial response received from {0}", mechName); SVNErrorManager.error(err, SVNLogType.NETWORK); } initialChallenge = toBase64(initialResponse); } if (initialChallenge != null) { getConnection().write("(w(s))", new Object[] {mechName, initialChallenge}); } else { getConnection().write("(w())", new Object[] {mechName}); } // read response (challenge) String status = SVNAuthenticator.STEP; while(SVNAuthenticator.STEP.equals(status)) { List items = getConnection().readTuple("w(?s)", true); status = (String) items.get(0); if (SVNAuthenticator.FAILURE.equals(status)) { String msg = (String) (items.size() > 1 ? items.get(1) : ""); setLastError(SVNErrorMessage.create(SVNErrorCode.RA_NOT_AUTHORIZED, msg)); return false; } String challenge = (String) (items.size() > 1 ? items.get(1) : null); if (challenge == null && ("CRAM-MD5".equals(mechName) || "GSSAPI".equals(mechName)) && SVNAuthenticator.SUCCESS.equals(status)) { challenge = ""; } if ((!SVNAuthenticator.STEP.equals(status) && !SVNAuthenticator.SUCCESS.equals(status)) || (challenge == null && expectChallenge)) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_NOT_AUTHORIZED, "Unexpected server response to authentication"); SVNErrorManager.error(err, SVNLogType.NETWORK); } byte[] challengeBytes = "CRAM-MD5".equals(mechName) ? challenge.getBytes() : fromBase64(challenge); byte[] response = null; if (!myClient.isComplete()) { response = myClient.evaluateChallenge(challengeBytes); } if (SVNAuthenticator.SUCCESS.equals(status)) { return true; } if (response == null) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_NOT_AUTHORIZED, "Unexpected response received from {0}", mechName); SVNErrorManager.error(err, SVNLogType.NETWORK); } if (response.length > 0) { String responseStr = "CRAM-MD5".equals(mechName) ? new String(response) : toBase64(response); getConnection().write("s", new Object[] {responseStr}); } else { getConnection().write("s", new Object[] {""}); } } return true; } protected void setEncryption(SVNRepositoryImpl repository) { if (getConnection().isEncrypted()) { dispose(); return; } String qop = (String) myClient.getNegotiatedProperty(Sasl.QOP); String buffSizeStr = (String) myClient.getNegotiatedProperty(Sasl.MAX_BUFFER); String sendSizeStr = (String) myClient.getNegotiatedProperty(Sasl.RAW_SEND_SIZE); if ("auth-int".equals(qop) || "auth-conf".equals(qop)) { int outBuffSize = 1000; int inBuffSize = 1000; if (sendSizeStr != null) { try { outBuffSize = Integer.parseInt(sendSizeStr); } catch (NumberFormatException nfe) { outBuffSize = 1000; } } if (buffSizeStr != null) { try { inBuffSize = Integer.parseInt(buffSizeStr); } catch (NumberFormatException nfe) { inBuffSize = 1000; } } SVNDebugLog.getDefaultLog().logFinest(SVNLogType.NETWORK, "SASL read buffer size: " + inBuffSize); SVNDebugLog.getDefaultLog().logFinest(SVNLogType.NETWORK, "SASL write buffer size: " + outBuffSize); try { getPlainOutputStream().flush(); } catch (IOException e) { // } OutputStream os = new SaslOutputStream(myClient, outBuffSize, getPlainOutputStream()); os = repository.getDebugLog().createLogStream(SVNLogType.NETWORK, os); setOutputStream(os); InputStream is = new SaslInputStream(myClient, inBuffSize, getPlainInputStream()); is = repository.getDebugLog().createLogStream(SVNLogType.NETWORK, is); setInputStream(is); getConnection().setEncrypted(this); } else { dispose(); } } protected SaslClient createSaslClient(List mechs, String realm, SVNRepositoryImpl repos, SVNURL location) throws SVNException { Map props = new SVNHashMap(); props.put(Sasl.QOP, "auth-conf,auth-int,auth"); props.put(Sasl.MAX_BUFFER, "8192"); props.put(Sasl.RAW_SEND_SIZE, "8192"); props.put(Sasl.POLICY_NOPLAINTEXT, "false"); props.put(Sasl.REUSE, "false"); props.put(Sasl.POLICY_NOANONYMOUS, "true"); String[] mechsArray = (String[]) mechs.toArray(new String[mechs.size()]); SaslClient client = null; for (int i = 0; i < mechsArray.length; i++) { String mech = mechsArray[i]; try { if ("ANONYMOUS".equals(mech) || "EXTERNAL".equals(mech) || "PLAIN".equals(mech)) { props.put(Sasl.POLICY_NOANONYMOUS, "false"); } SaslClientFactory clientFactory = getSaslClientFactory(mech, props); if (clientFactory == null) { continue; } SVNAuthentication auth = null; if ("ANONYMOUS".equals(mech)) { auth = SVNPasswordAuthentication.newInstance("", new char[0], false, location, false); } else if ("EXTERNAL".equals(mech)) { String name = repos.getExternalUserName(); if (name == null) { name = ""; } auth = SVNPasswordAuthentication.newInstance(name, new char[0], false, location, false); } else { if (myAuthenticationManager == null) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_NOT_AUTHORIZED, "Authentication required for ''{0}''", realm), SVNLogType.NETWORK); } String realmName = getFullRealmName(location, realm); if (myAuthentication != null) { myAuthentication = myAuthenticationManager.getNextAuthentication(ISVNAuthenticationManager.PASSWORD, realmName, location); } else { myAuthentication = myAuthenticationManager.getFirstAuthentication(ISVNAuthenticationManager.PASSWORD, realmName, location); } if (myAuthentication == null) { if (getLastError() != null) { SVNErrorManager.error(getLastError(), SVNLogType.NETWORK); } SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_NOT_AUTHORIZED, "Authentication required for ''{0}''", realm), SVNLogType.NETWORK); } auth = myAuthentication; } client = clientFactory.createSaslClient(new String[] {"ANONYMOUS".equals(mech) ? "PLAIN" : mech}, null, "svn", location.getHost(), props, new SVNCallbackHandler(realm, auth)); if (client != null) { break; } myAuthentication = null; } catch (SaslException e) { // remove mech from the list and try next // so next time we wouldn't even try this mech next time. mechs.remove(mechsArray[i]); myAuthentication = null; } } return client; } private static String getFullRealmName(SVNURL location, String realm) { if (location == null || realm == null) { return realm; } return "<" + location.getProtocol() + "://" + location.getHost() + ":" + location.getPort() + "> " + realm; } private static String toBase64(byte[] src) { return SVNBase64.byteArrayToBase64(src); } private static byte[] fromBase64(String src) { if (src == null) { return new byte[0]; } ByteArrayOutputStream bos = new ByteArrayOutputStream(); for (int i = 0; i < src.length(); i++) { char ch = src.charAt(i); if (!Character.isWhitespace(ch) && ch != '\n' && ch != '\r') { bos.write((byte) ch & 0xFF); } } byte[] cbytes = new byte[src.length()]; try { src = new String(bos.toByteArray(), "US-ASCII"); } catch (UnsupportedEncodingException e) { // } int clength = SVNBase64.base64ToByteArray(new StringBuffer(src), cbytes); byte[] result = new byte[clength]; // strip trailing -1s. for(int i = clength - 1; i>=0; i--) { if (i == -1) { clength--; } } System.arraycopy(cbytes, 0, result, 0, clength); return result; } private static String getMechanismName(SaslClient client, boolean isAnonymous) { if (client == null) { return null; } String name = client.getMechanismName(); if ("PLAIN".equals(name) && isAnonymous) { name = "ANONYMOUS"; } return name; } private static SaslClientFactory getSaslClientFactory(String mechName, Map props) { if (mechName == null) { return null; } if ("ANONYMOUS".equals(mechName)) { mechName = "PLAIN"; } for(Enumeration factories = Sasl.getSaslClientFactories(); factories.hasMoreElements();) { SaslClientFactory factory = (SaslClientFactory) factories.nextElement(); String[] mechs = factory.getMechanismNames(props); for (int i = 0; mechs != null && i < mechs.length; i++) { if (mechName.endsWith(mechs[i])) { return factory; } } } return null; } private static class SVNCallbackHandler implements CallbackHandler { private String myRealm; private SVNAuthentication myAuthentication; public SVNCallbackHandler(String realm, SVNAuthentication auth) { myRealm = realm; myAuthentication = auth; } public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { for (int i = 0; i < callbacks.length; i++) { Callback callback = callbacks[i]; if (callback instanceof NameCallback) { String userName = myAuthentication.getUserName(); ((NameCallback) callback).setName(userName != null ? userName : ""); } else if (callback instanceof PasswordCallback) { final char[] password = ((SVNPasswordAuthentication) myAuthentication).getPasswordValue(); ((PasswordCallback) callback).setPassword(password != null ? password : new char[0]); } else if (callback instanceof RealmCallback) { ((RealmCallback) callback).setText(myRealm); } else { throw new UnsupportedCallbackException(callback); } } } } }