# HG changeset patch # User Artem Tikhomirov # Date 1375982330 -7200 # Node ID a483b2b68a2e295a160ade48197e65319094f79b # Parent 822f3a83ff576e10da4591dda5b31694f8b098cb Provisional APIs and respective implementation for http, https and ssh remote repositories diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/auth/HgAuthFailedException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/auth/HgAuthFailedException.java Thu Aug 08 19:18:50 2013 +0200 @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.auth; + +import org.tmatesoft.hg.internal.Experimental; + +/** + * FIXME split and describe + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + * @since 1.2 + */ +@SuppressWarnings("serial") +@Experimental(reason="Provisional API. Work in progress") +public class HgAuthFailedException extends Exception /*XXX HgRemoteException?*/ { + public HgAuthFailedException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/auth/HgAuthMethod.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/auth/HgAuthMethod.java Thu Aug 08 19:18:50 2013 +0200 @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.auth; + +import java.io.InputStream; +import java.security.cert.X509Certificate; + +import org.tmatesoft.hg.internal.Experimental; + +/** + * Clients do not implement this interface, instead, they invoke appropriate authentication method + * once they got user input + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + * @since 1.2 + */ +@Experimental(reason="Provisional API. Work in progress") +public interface HgAuthMethod { + public void noCredentials() throws HgAuthFailedException; + public boolean supportsPassword(); + public void withPassword(String username, String password) throws HgAuthFailedException; + public boolean supportsPublicKey(); + public void withPublicKey(String username, InputStream privateKey, String passphrase) throws HgAuthFailedException; + public boolean supportsCertificate(); + public void withCertificate(X509Certificate[] clientCertChain) throws HgAuthFailedException; +} \ No newline at end of file diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/auth/HgAuthenticator.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/auth/HgAuthenticator.java Thu Aug 08 19:18:50 2013 +0200 @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.auth; + +import org.tmatesoft.hg.core.SessionContext; +import org.tmatesoft.hg.internal.Experimental; +import org.tmatesoft.hg.repo.HgRemoteRepository.RemoteDescriptor; + +/** + * Client may implement this interface if they need more control over authentication process. + * + * @see SessionContext#getAuthenticator(RemoteDescriptor) + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + * @since 1.2 + */ +@Experimental(reason="Provisional API. Work in progress") +public interface HgAuthenticator { + // XXX either another AuthMethod or a separate #authenticate + // to perform server check. Alternatively, as methods in AuthMethod + public void authenticate(RemoteDescriptor rd, HgAuthMethod authMethod) throws HgAuthFailedException; +} \ No newline at end of file diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/auth/package.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/auth/package.html Thu Aug 08 19:18:50 2013 +0200 @@ -0,0 +1,6 @@ + + +

Authentication API

+

Classes and interfaces to support authentication for remote connections

+ + \ No newline at end of file diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/core/SessionContext.java --- a/src/org/tmatesoft/hg/core/SessionContext.java Tue Aug 06 21:18:33 2013 +0200 +++ b/src/org/tmatesoft/hg/core/SessionContext.java Thu Aug 08 19:18:50 2013 +0200 @@ -18,10 +18,12 @@ import java.net.URI; +import org.tmatesoft.hg.auth.HgAuthenticator; import org.tmatesoft.hg.internal.BasicSessionContext; import org.tmatesoft.hg.internal.Experimental; +import org.tmatesoft.hg.internal.remote.BasicAuthenticator; import org.tmatesoft.hg.internal.remote.RemoteConnectorDescriptor; -import org.tmatesoft.hg.repo.HgLookup.RemoteDescriptor; +import org.tmatesoft.hg.repo.HgRemoteRepository; import org.tmatesoft.hg.util.LogFacility; import org.tmatesoft.hg.util.Path; @@ -84,10 +86,19 @@ * * @return null if supplied URI doesn't point to a remote repository or repositories of that kind are not supported */ - @Experimental(reason="Work in progress, provisional API") - public RemoteDescriptor getRemoteDescriptor(URI uri) { + @Experimental(reason="Provisional API. Work in progress") + public HgRemoteRepository.RemoteDescriptor getRemoteDescriptor(URI uri) { return new RemoteConnectorDescriptor.Provider().get(this, uri); } + + /** + * Facility to perform authentication for a given remote connection + * @return never null + */ + @Experimental(reason="Provisional API. Work in progress") + public HgAuthenticator getAuthenticator(HgRemoteRepository.RemoteDescriptor rd) { + return new BasicAuthenticator(getLog()); + } /** * Providers of the context may implement diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/internal/remote/BasicAuthenticator.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/remote/BasicAuthenticator.java Thu Aug 08 19:18:50 2013 +0200 @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal.remote; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import org.tmatesoft.hg.auth.HgAuthFailedException; +import org.tmatesoft.hg.auth.HgAuthMethod; +import org.tmatesoft.hg.auth.HgAuthenticator; +import org.tmatesoft.hg.repo.HgRemoteRepository.RemoteDescriptor; +import org.tmatesoft.hg.util.LogFacility; +import org.tmatesoft.hg.util.LogFacility.Severity; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class BasicAuthenticator implements HgAuthenticator { + private final LogFacility log; + + public BasicAuthenticator(LogFacility logFacility) { + log = logFacility; + } + + public void authenticate(RemoteDescriptor rd, HgAuthMethod authMethod) throws HgAuthFailedException { + if (authMethod.supportsPublicKey()) { + if (tryPlatformDefaultKeyLocations(rd, authMethod)) { + return; + } + } + authMethod.noCredentials(); + } + + // return true is successfully aithenticated + protected boolean tryPlatformDefaultKeyLocations(RemoteDescriptor rd, HgAuthMethod authMethod) { + final String userHome = System.getProperty("user.home"); + File sshDir = new File(userHome, ".ssh"); + if (!sshDir.isDirectory()) { + return false; + } + final String username = System.getProperty("user.name"); + for (String fn : new String[] { "id_rsa", "id_dsa", "identity"}) { + File id = new File(sshDir, fn); + if (!id.canRead()) { + continue; + } + try { + FileInputStream fis = new FileInputStream(id); + authMethod.withPublicKey(username, fis, null); + fis.close(); + return true; + } catch (IOException ex) { + log.dump(getClass(), Severity.Warn, ex, String.format("Attempting default ssh identity key locations: %s", id)); + // ignore + } catch (HgAuthFailedException ex) { + log.dump(getClass(), Severity.Debug, ex, String.format("Attempting default ssh identity key locations: %s", id)); + // ignore + } + } + return false; + } +} \ No newline at end of file diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/internal/remote/Connector.java --- a/src/org/tmatesoft/hg/internal/remote/Connector.java Tue Aug 06 21:18:33 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/remote/Connector.java Thu Aug 08 19:18:50 2013 +0200 @@ -18,14 +18,15 @@ import java.io.InputStream; import java.io.OutputStream; -import java.net.URI; import java.util.Collection; import java.util.List; +import org.tmatesoft.hg.auth.HgAuthFailedException; import org.tmatesoft.hg.core.HgRemoteConnectionException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.repo.HgRemoteRepository.Range; +import org.tmatesoft.hg.repo.HgRemoteRepository.RemoteDescriptor; import org.tmatesoft.hg.repo.HgRuntimeException; /** @@ -46,10 +47,11 @@ static final String NS_BOOKMARKS = "bookmarks"; static final String NS_PHASES = "phases"; - void init(URI uri, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException; + // note, #init shall not assume remote is instanceof RemoteConnectorDescriptor, but Adaptable to it, instead + void init(RemoteDescriptor remote, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException; String getServerLocation(); // - void connect() throws HgRemoteConnectionException, HgRuntimeException; + void connect() throws HgAuthFailedException, HgRemoteConnectionException, HgRuntimeException; void disconnect() throws HgRemoteConnectionException, HgRuntimeException; void sessionBegin() throws HgRemoteConnectionException, HgRuntimeException; void sessionEnd() throws HgRemoteConnectionException, HgRuntimeException; diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/internal/remote/ConnectorBase.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/remote/ConnectorBase.java Thu Aug 08 19:18:50 2013 +0200 @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal.remote; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.util.Collection; +import java.util.List; + +import org.tmatesoft.hg.auth.HgAuthFailedException; +import org.tmatesoft.hg.core.HgRemoteConnectionException; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.core.SessionContext; +import org.tmatesoft.hg.repo.HgRemoteRepository.RemoteDescriptor; +import org.tmatesoft.hg.repo.HgRuntimeException; +import org.tmatesoft.hg.repo.HgRemoteRepository.Range; + +/** + * An abstract base class for {@link Connector} implementations, + * to keep binary compatibility once {@link Connector} interface changes. + * + *

Provides default implementation for {@link #getServerLocation()} that hides user credentials from uri, if any + * + *

Present method implementations are not expected to be invoked and do nothing, this may change in future to return + * reasonable error objects. New methods, added to {@link Connector}, will get default implementation in this class as well. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public abstract class ConnectorBase implements Connector { + protected URI uri; + + protected ConnectorBase() { + } + + protected void setURI(URI uri) { + this.uri = uri; + } + + // clients may invoke this method, or call #setURI(URI) directly + public void init(RemoteDescriptor remote, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException { + setURI(remote.getURI()); + } + + public String getServerLocation() { + if (uri == null) { + return ""; + } + if (uri.getUserInfo() == null) { + return uri.toString(); + } + if (uri.getPort() != -1) { + return String.format("%s://%s:%d%s", uri.getScheme(), uri.getHost(), uri.getPort(), uri.getPath()); + } else { + return String.format("%s://%s%s", uri.getScheme(), uri.getHost(), uri.getPath()); + } + } + + public void connect() throws HgAuthFailedException, HgRemoteConnectionException, HgRuntimeException { + } + + public void disconnect() throws HgRemoteConnectionException, HgRuntimeException { + } + + public void sessionBegin() throws HgRemoteConnectionException, HgRuntimeException { + } + + public void sessionEnd() throws HgRemoteConnectionException, HgRuntimeException { + } + + public String getCapabilities() throws HgRemoteConnectionException, HgRuntimeException { + return null; + } + + public InputStream heads() throws HgRemoteConnectionException, HgRuntimeException { + return null; + } + + public InputStream between(Collection ranges) throws HgRemoteConnectionException, HgRuntimeException { + return null; + } + + public InputStream branches(List nodes) throws HgRemoteConnectionException, HgRuntimeException { + return null; + } + + public InputStream changegroup(List roots) throws HgRemoteConnectionException, HgRuntimeException { + return null; + } + + public OutputStream unbundle(long outputLen, List remoteHeads) throws HgRemoteConnectionException, HgRuntimeException { + return null; + } + + public InputStream pushkey(String opName, String namespace, String key, String oldValue, String newValue) throws HgRemoteConnectionException, HgRuntimeException { + return null; + } + + public InputStream listkeys(String namespace, String actionName) throws HgRemoteConnectionException, HgRuntimeException { + return null; + } +} diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/internal/remote/HttpAuthMethod.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/remote/HttpAuthMethod.java Thu Aug 08 19:18:50 2013 +0200 @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal.remote; + +import static org.tmatesoft.hg.util.LogFacility.Severity.Info; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.tmatesoft.hg.auth.HgAuthFailedException; +import org.tmatesoft.hg.auth.HgAuthMethod; +import org.tmatesoft.hg.core.HgRemoteConnectionException; +import org.tmatesoft.hg.core.SessionContext; +import org.tmatesoft.hg.repo.HgInvalidStateException; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HttpAuthMethod implements HgAuthMethod { + + private final SessionContext ctx; + private final URL url; + private String authInfo; + private SSLContext sslContext; + + /** + * @param sessionContext + * @param url location fully ready to attempt connection to perform authentication check, e.g. hello command (anything with *small* output will do) + * @throws HgRemoteConnectionException + */ + HttpAuthMethod(SessionContext sessionContext, URL url) throws HgRemoteConnectionException { + ctx = sessionContext; + if (!"http".equals(url.getProtocol()) && !"https".equals(url.getProtocol())) { + throw new HgInvalidStateException(String.format("http protocol expected: %s", url.toString())); + } + this.url = url; + if ("https".equals(url.getProtocol())) { + try { + sslContext = SSLContext.getInstance("SSL"); + class TrustEveryone implements X509TrustManager { + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + sslContext.init(null, new TrustManager[] { new TrustEveryone() }, null); + } catch (Exception ex) { + throw new HgRemoteConnectionException("Can't initialize secure connection", ex); + } + } else { + sslContext = null; + } + } + + public void tryWithUserInfo(String uriUserInfo) throws HgAuthFailedException { + int colon = uriUserInfo.indexOf(':'); + if (colon == -1) { + withPassword(uriUserInfo, null); + } else { + withPassword(uriUserInfo.substring(0, colon), uriUserInfo.substring(colon+1)); + } + } + + public void noCredentials() throws HgAuthFailedException { + // TODO Auto-generated method stub + checkConnection(); + } + + public boolean supportsPassword() { + return true; + } + + public void withPassword(String username, String password) throws HgAuthFailedException { + authInfo = buildAuthValue(username, password == null ? "" : password); + checkConnection(); + } + + public boolean supportsPublicKey() { + return false; + } + + public void withPublicKey(String username, InputStream privateKey, String passphrase) throws HgAuthFailedException { + } + + public boolean supportsCertificate() { + return "https".equals(url.getProtocol()); + } + + public void withCertificate(X509Certificate[] clientCert) throws HgAuthFailedException { + // TODO Auto-generated method stub + checkConnection(); + } + + private void checkConnection() throws HgAuthFailedException { + // we've checked the protocol to be http(s) + HttpURLConnection c = null; + try { + c = (HttpURLConnection) url.openConnection(); + c = setupConnection(c); + c.connect(); + InputStream is = c.getInputStream(); + while (is.read() != -1) { + } + is.close(); + final int HTTP_UNAUTHORIZED = 401; + if (c.getResponseCode() == HTTP_UNAUTHORIZED) { + throw new HgAuthFailedException(c.getResponseMessage(), null); + } + } catch (IOException ex) { + throw new HgAuthFailedException("Communication failure while authenticating", ex); + } finally { + if (c != null) { + c.disconnect(); + } + } + } + + HttpURLConnection setupConnection(HttpURLConnection urlConnection) { + if (authInfo != null) { + urlConnection.addRequestProperty("Authorization", "Basic " + authInfo); + } + if (sslContext != null) { + ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslContext.getSocketFactory()); + } + return urlConnection; + } + + private String buildAuthValue(String username, String password) { + String ai = null; + try { + // Hack to get Base64-encoded credentials + Preferences tempNode = Preferences.userRoot().node("xxx"); + tempNode.putByteArray("xxx", String.format("%s:%s", username, password).getBytes()); + ai = tempNode.get("xxx", null); + tempNode.removeNode(); + } catch (BackingStoreException ex) { + ctx.getLog().dump(getClass(), Info, ex, null); + // IGNORE + } + return ai; + } +} diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/internal/remote/HttpConnector.java --- a/src/org/tmatesoft/hg/internal/remote/HttpConnector.java Tue Aug 06 21:18:33 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/remote/HttpConnector.java Thu Aug 08 19:18:50 2013 +0200 @@ -16,8 +16,6 @@ */ package org.tmatesoft.hg.internal.remote; -import static org.tmatesoft.hg.util.LogFacility.Severity.Info; - import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.FilterOutputStream; @@ -28,27 +26,20 @@ import java.io.SequenceInputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; -import java.net.URI; import java.net.URL; import java.net.URLConnection; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.prefs.BackingStoreException; -import java.util.prefs.Preferences; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - +import org.tmatesoft.hg.auth.HgAuthFailedException; +import org.tmatesoft.hg.auth.HgAuthenticator; import org.tmatesoft.hg.core.HgRemoteConnectionException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.internal.PropertyMarshal; import org.tmatesoft.hg.repo.HgRemoteRepository.Range; +import org.tmatesoft.hg.repo.HgRemoteRepository.RemoteDescriptor; import org.tmatesoft.hg.repo.HgRuntimeException; /** @@ -56,69 +47,43 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class HttpConnector implements Connector { - private URI uri; +public class HttpConnector extends ConnectorBase { + private RemoteDescriptor rd; private URL url; - private SSLContext sslContext; - private String authInfo; private boolean debug; private SessionContext sessionCtx; // private HttpURLConnection conn; + private HttpAuthMethod authMediator; - public void init(URI uri, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException { - this.uri = uri; + public void init(RemoteDescriptor remote, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException { + rd = remote; + setURI(remote.getURI()); sessionCtx = sessionContext; - debug = new PropertyMarshal(sessionCtx).getBoolean("hg4j.remote.debug", false); - if (uri.getUserInfo() != null) { - String ai = null; - try { - // Hack to get Base64-encoded credentials - Preferences tempNode = Preferences.userRoot().node("xxx"); - tempNode.putByteArray("xxx", uri.getUserInfo().getBytes()); - ai = tempNode.get("xxx", null); - tempNode.removeNode(); - } catch (BackingStoreException ex) { - sessionContext.getLog().dump(getClass(), Info, ex, null); - // IGNORE - } - authInfo = ai; - } else { - authInfo = null; - } + debug = new PropertyMarshal(sessionContext).getBoolean("hg4j.remote.debug", false); } - public void connect() throws HgRemoteConnectionException, HgRuntimeException { + public void connect() throws HgAuthFailedException, HgRemoteConnectionException, HgRuntimeException { try { url = uri.toURL(); } catch (MalformedURLException ex) { throw new HgRemoteConnectionException("Bad URL", ex); } - if ("https".equals(url.getProtocol())) { + authMediator = new HttpAuthMethod(sessionCtx, url); + authenticateClient(); + } + + private void authenticateClient() throws HgAuthFailedException { + String userInfo = url.getUserInfo(); + if (userInfo != null) { try { - sslContext = SSLContext.getInstance("SSL"); - class TrustEveryone implements X509TrustManager { - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - if (debug) { - System.out.println("checkClientTrusted:" + authType); - } - } - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - if (debug) { - System.out.println("checkServerTrusted:" + authType); - } - } - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - }; - sslContext.init(null, new TrustManager[] { new TrustEveryone() }, null); - } catch (Exception ex) { - throw new HgRemoteConnectionException("Can't initialize secure connection", ex); + authMediator.tryWithUserInfo(userInfo); + } catch (HgAuthFailedException ex) { + // FALL THROUGH to try Authenticator } - } else { - sslContext = null; } + HgAuthenticator auth = sessionCtx.getAuthenticator(rd); + auth.authenticate(rd, authMediator); } public void disconnect() throws HgRemoteConnectionException, HgRuntimeException { @@ -138,17 +103,6 @@ } } - public String getServerLocation() { - if (uri.getUserInfo() == null) { - return uri.toString(); - } - if (uri.getPort() != -1) { - return String.format("%s://%s:%d%s", uri.getScheme(), uri.getHost(), uri.getPort(), uri.getPath()); - } else { - return String.format("%s://%s%s", uri.getScheme(), uri.getHost(), uri.getPath()); - } - } - public String getCapabilities() throws HgRemoteConnectionException { // say hello to server, check response try { @@ -367,13 +321,7 @@ private HttpURLConnection setupConnection(URLConnection urlConnection) { urlConnection.setRequestProperty("User-Agent", "hg4j/1.0.0"); urlConnection.addRequestProperty("Accept", "application/mercurial-0.1"); - if (authInfo != null) { - urlConnection.addRequestProperty("Authorization", "Basic " + authInfo); - } - if (sslContext != null) { - ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslContext.getSocketFactory()); - } - return (HttpURLConnection) urlConnection; + return authMediator.setupConnection((HttpURLConnection) urlConnection); } private StringBuilder appendNodeidListArgument(String key, List values, StringBuilder sb) { diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/internal/remote/RemoteConnectorDescriptor.java --- a/src/org/tmatesoft/hg/internal/remote/RemoteConnectorDescriptor.java Tue Aug 06 21:18:33 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/remote/RemoteConnectorDescriptor.java Thu Aug 08 19:18:50 2013 +0200 @@ -22,8 +22,6 @@ import org.tmatesoft.hg.core.HgBadArgumentException; import org.tmatesoft.hg.core.SessionContext; -import org.tmatesoft.hg.repo.HgLookup.Authenticator; -import org.tmatesoft.hg.repo.HgLookup.RemoteDescriptor; import org.tmatesoft.hg.repo.HgRemoteRepository; import org.tmatesoft.hg.util.Pair; @@ -38,7 +36,7 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class RemoteConnectorDescriptor implements RemoteDescriptor { +public class RemoteConnectorDescriptor implements HgRemoteRepository.RemoteDescriptor { private Map> connFactory; private final URI uri; @@ -56,11 +54,6 @@ return uri; } - public Authenticator getAuth() { - // TODO Auto-generated method stub - return null; - } - public Connector createConnector() throws HgBadArgumentException { Pair connectorToBe = connFactory.get(uri.getScheme()); if (connectorToBe == null || connectorToBe.second() == null) { @@ -95,7 +88,7 @@ knownConnectors.put("ssh", new Pair(cl, SshConnector.class.getName())); } - public RemoteDescriptor get(SessionContext ctx, URI uri) { + public HgRemoteRepository.RemoteDescriptor get(SessionContext ctx, URI uri) { if (knownConnectors.containsKey(uri.getScheme())) { return new RemoteConnectorDescriptor(knownConnectors, uri); } diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/internal/remote/SshAuthMethod.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/remote/SshAuthMethod.java Thu Aug 08 19:18:50 2013 +0200 @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal.remote; + +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.InputStream; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import org.tmatesoft.hg.auth.HgAuthFailedException; +import org.tmatesoft.hg.auth.HgAuthMethod; + +import com.trilead.ssh2.Connection; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public final class SshAuthMethod implements HgAuthMethod { + + private final Connection conn; + + public SshAuthMethod(Connection connection) { + conn = connection; + } + + public void tryWithUserInfo(String uriUserInfo) throws HgAuthFailedException { + assert uriUserInfo != null && uriUserInfo.trim().length() > 0; + int colon = uriUserInfo.indexOf(':'); + if (colon == -1) { + String username = uriUserInfo; + withPassword(username, null); + } else { + String username = uriUserInfo.substring(0, colon); + String password = uriUserInfo.substring(colon+1); + withPassword(username, password); + } + return; + } + + public void noCredentials() throws HgAuthFailedException { + try { + String username = System.getProperty("user.name"); + if (!conn.authenticateWithNone(username)) { + throw authFailed(username); + } + } catch (IOException ex) { + throw commFailed(ex); + } + } + + public void withPublicKey(String username, InputStream privateKey, String passphrase) throws HgAuthFailedException { + if (username == null) { + // FIXME AuthFailure and AuthFailed or similar distinct exceptions to tell true authentication issues from + // failures around it. + throw new HgAuthFailedException("Need username", null); + } + if (privateKey == null) { + throw new HgAuthFailedException("Need private key", null); + } + CharArrayWriter a = new CharArrayWriter(2048); + int r; + try { + while((r = privateKey.read()) != -1) { + a.append((char) r); + } + } catch (IOException ex) { + throw new HgAuthFailedException("Failed to read private key", ex); + } + try { + boolean success = conn.authenticateWithPublicKey(username, a.toCharArray(), passphrase); + if (!success) { + throw authFailed(username); + } + } catch (IOException ex) { + throw commFailed(ex); + } + } + + public void withPassword(String username, String password) throws HgAuthFailedException { + if (username == null) { + throw new HgAuthFailedException("Need username", null); + } + try { + boolean success; + if (password == null) { + success = conn.authenticateWithNone(username); + } else { + success = conn.authenticateWithPassword(username, password); + } + if (!success) { + throw authFailed(username); + } + } catch (IOException ex) { + throw commFailed(ex); + } + } + + public void withCertificate(X509Certificate[] clientCert) throws HgAuthFailedException { + } + + public boolean supportsPublicKey() { + return true; + } + + public boolean supportsPassword() { + return true; + } + + public boolean supportsCertificate() { + return true; + } + + private HgAuthFailedException commFailed(IOException ex) { + return new HgAuthFailedException("Communication failure while authenticating", ex); + } + + private HgAuthFailedException authFailed(String username) throws IOException { + final String[] authMethodsLeft = conn.getRemainingAuthMethods(username); + return new HgAuthFailedException(String.format("Failed to authenticate, other methods to try: %s", Arrays.toString(authMethodsLeft)), null); + } +} \ No newline at end of file diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/internal/remote/SshConnector.java --- a/src/org/tmatesoft/hg/internal/remote/SshConnector.java Tue Aug 06 21:18:33 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/remote/SshConnector.java Thu Aug 08 19:18:50 2013 +0200 @@ -20,7 +20,6 @@ import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.EOFException; -import java.io.File; import java.io.FilterInputStream; import java.io.FilterOutputStream; import java.io.IOException; @@ -28,21 +27,22 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.SequenceInputStream; -import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import org.tmatesoft.hg.auth.HgAuthFailedException; +import org.tmatesoft.hg.auth.HgAuthenticator; import org.tmatesoft.hg.core.HgRemoteConnectionException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.repo.HgRemoteRepository.Range; +import org.tmatesoft.hg.repo.HgRemoteRepository.RemoteDescriptor; import org.tmatesoft.hg.repo.HgRuntimeException; import org.tmatesoft.hg.util.LogFacility.Severity; import com.trilead.ssh2.Connection; -import com.trilead.ssh2.ConnectionInfo; import com.trilead.ssh2.Session; import com.trilead.ssh2.StreamGobbler; @@ -52,9 +52,9 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class SshConnector implements Connector { +public class SshConnector extends ConnectorBase { + private RemoteDescriptor rd; private SessionContext sessionCtx; - private URI uri; private Connection conn; private Session session; private int sessionUse; @@ -62,30 +62,39 @@ private StreamGobbler remoteErr, remoteOut; private OutputStream remoteIn; - public void init(URI uri, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException { + public void init(RemoteDescriptor remote, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException { + rd = remote; sessionCtx = sessionContext; - this.uri = uri; + setURI(remote.getURI()); } - public void connect() throws HgRemoteConnectionException, HgRuntimeException { + public void connect() throws HgAuthFailedException, HgRemoteConnectionException, HgRuntimeException { try { conn = new Connection(uri.getHost(), uri.getPort() == -1 ? 22 : uri.getPort()); conn.connect(); + authenticateClient(); } catch (IOException ex) { - throw new HgRemoteConnectionException("Failed to establish connection"); + throw new HgRemoteConnectionException("Failed to establish connection").setServerInfo(getServerLocation()); } - try { - conn.authenticateWithPublicKey(System.getProperty("user.name"), new File(System.getProperty("user.home"), ".ssh/id_rsa"), null); - ConnectionInfo ci = conn.getConnectionInfo(); - System.out.printf("%s %s %s %d %s %s %s\n", ci.clientToServerCryptoAlgorithm, ci.clientToServerMACAlgorithm, ci.keyExchangeAlgorithm, ci.keyExchangeCounter, ci.serverHostKeyAlgorithm, ci.serverToClientCryptoAlgorithm, ci.serverToClientMACAlgorithm); - } catch (IOException ex) { - throw new HgRemoteConnectionException("Failed to authenticate", ex).setServerInfo(getServerLocation()); + } + + private void authenticateClient() throws HgAuthFailedException { + SshAuthMethod m = new SshAuthMethod(conn); + if (uri.getUserInfo() != null) { + try { + m.tryWithUserInfo(uri.getUserInfo()); + return; + } catch (HgAuthFailedException ex) { + // FALL-THROUGH to try with Authenticator + } } + HgAuthenticator auth = sessionCtx.getAuthenticator(rd); + auth.authenticate(rd, m); } public void disconnect() throws HgRemoteConnectionException { if (session != null) { - forceSessionClose(); + doSessionClose(); } if (conn != null) { conn.close(); @@ -119,13 +128,9 @@ sessionUse--; return; } - forceSessionClose(); + doSessionClose(); } - public String getServerLocation() { - return uri.toString(); // FIXME - } - public String getCapabilities() throws HgRemoteConnectionException { try { consume(remoteOut); @@ -291,7 +296,7 @@ } - private void forceSessionClose() { + private void doSessionClose() { if (session != null) { closeQuietly(remoteErr); closeQuietly(remoteOut); diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/repo/HgLookup.java --- a/src/org/tmatesoft/hg/repo/HgLookup.java Tue Aug 06 21:18:33 2013 +0200 +++ b/src/org/tmatesoft/hg/repo/HgLookup.java Thu Aug 08 19:18:50 2013 +0200 @@ -19,7 +19,6 @@ import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; import java.io.File; -import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -33,7 +32,6 @@ import org.tmatesoft.hg.internal.BasicSessionContext; import org.tmatesoft.hg.internal.ConfigFile; import org.tmatesoft.hg.internal.DataAccessProvider; -import org.tmatesoft.hg.internal.Experimental; import org.tmatesoft.hg.internal.RequiresFile; import org.tmatesoft.hg.repo.HgRepoConfig.PathsSection; @@ -184,7 +182,7 @@ * @throws HgBadArgumentException */ public HgRemoteRepository detectRemote(URI uriRemote) throws HgBadArgumentException { - RemoteDescriptor rd = getSessionContext().getRemoteDescriptor(uriRemote); + HgRemoteRepository.RemoteDescriptor rd = getSessionContext().getRemoteDescriptor(uriRemote); if (rd == null) { throw new HgBadArgumentException(String.format("Unsupported remote repository location:%s", uriRemote), null); } @@ -210,41 +208,4 @@ } return sessionContext; } - - - /** - * Session context ({@link SessionContext#getRemoteDescriptor(URI)} gives descriptor of remote when asked. - */ - @Experimental(reason="Work in progress") - public interface RemoteDescriptor { - URI getURI(); - Authenticator getAuth(); - } - - @Experimental(reason="Work in progress") - public interface Authenticator { - public void authenticate(RemoteDescriptor remote, AuthMethod authMethod); - } - - /** - * Clients do not implement this interface, instead, they invoke appropriate authentication method - * once they got user input - */ - @Experimental(reason="Work in progress") - public interface AuthMethod { - public void noCredentials() throws AuthFailedException; - public boolean supportsPassword(); - public void withPassword(String username, byte[] password) throws AuthFailedException; - public boolean supportsPublicKey(); - public void withPublicKey(String username, InputStream publicKey, String passphrase) throws AuthFailedException; - public boolean supportsCertificate(); - public void withCertificate() throws AuthFailedException; - } - - @SuppressWarnings("serial") - public class AuthFailedException extends Exception /*XXX HgRemoteException?*/ { - public AuthFailedException(String message, Throwable cause) { - super(message, cause); - } - } } diff -r 822f3a83ff57 -r a483b2b68a2e src/org/tmatesoft/hg/repo/HgRemoteRepository.java --- a/src/org/tmatesoft/hg/repo/HgRemoteRepository.java Tue Aug 06 21:18:33 2013 +0200 +++ b/src/org/tmatesoft/hg/repo/HgRemoteRepository.java Thu Aug 08 19:18:50 2013 +0200 @@ -27,6 +27,7 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.StreamTokenizer; +import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -39,6 +40,7 @@ import java.util.Map; import java.util.Set; +import org.tmatesoft.hg.auth.HgAuthFailedException; import org.tmatesoft.hg.core.HgBadArgumentException; import org.tmatesoft.hg.core.HgIOException; import org.tmatesoft.hg.core.HgRemoteConnectionException; @@ -49,12 +51,13 @@ import org.tmatesoft.hg.internal.DataSerializer; import org.tmatesoft.hg.internal.DataSerializer.OutputStreamSerializer; import org.tmatesoft.hg.internal.EncodingHelper; +import org.tmatesoft.hg.internal.Experimental; import org.tmatesoft.hg.internal.FileUtils; import org.tmatesoft.hg.internal.Internals; import org.tmatesoft.hg.internal.PropertyMarshal; import org.tmatesoft.hg.internal.remote.Connector; import org.tmatesoft.hg.internal.remote.RemoteConnectorDescriptor; -import org.tmatesoft.hg.repo.HgLookup.RemoteDescriptor; +import org.tmatesoft.hg.util.Adaptable; import org.tmatesoft.hg.util.LogFacility.Severity; import org.tmatesoft.hg.util.Outcome; import org.tmatesoft.hg.util.Pair; @@ -77,13 +80,14 @@ private Connector remote; HgRemoteRepository(SessionContext ctx, RemoteDescriptor rd) throws HgBadArgumentException { - if (false == rd instanceof RemoteConnectorDescriptor) { + RemoteConnectorDescriptor rcd = Adaptable.Factory.getAdapter(rd, RemoteConnectorDescriptor.class, null); + if (rcd == null) { throw new IllegalArgumentException(String.format("Present implementation supports remote connections via %s only", Connector.class.getName())); } sessionContext = ctx; debug = new PropertyMarshal(ctx).getBoolean("hg4j.remote.debug", false); - remote = ((RemoteConnectorDescriptor) rd).createConnector(); - remote.init(rd.getURI(), ctx, null); + remote = rcd.createConnector(); + remote.init(rd /*sic! pass original*/, ctx, null); } public boolean isInvalid() throws HgRemoteConnectionException { @@ -369,7 +373,11 @@ if (remoteCapabilities != null) { return; } - remote.connect(); + try { + remote.connect(); + } catch (HgAuthFailedException ex) { + throw new HgRemoteConnectionException("Failed to authenticate", ex).setServerInfo(remote.getServerLocation()); + } try { remote.sessionBegin(); String capsLine = remote.getCapabilities(); @@ -540,4 +548,19 @@ return pub; } } + + /** + * Session context ({@link SessionContext#getRemoteDescriptor(URI)} gives descriptor of remote when asked. + * Clients may supply own descriptors e.g. if need to pass extra information into Authenticator. + * Present implementation of {@link HgRemoteRepository} will be happy with any {@link RemoteDescriptor} subclass + * as long as it's {@link Adaptable adaptable} to {@link RemoteConnectorDescriptor} + * @since 1.2 + */ + @Experimental(reason="Provisional API. Work in progress") + public interface RemoteDescriptor { + /** + * @return remote location, never null + */ + URI getURI(); + } } diff -r 822f3a83ff57 -r a483b2b68a2e test/org/tmatesoft/hg/test/Configuration.java --- a/test/org/tmatesoft/hg/test/Configuration.java Tue Aug 06 21:18:33 2013 +0200 +++ b/test/org/tmatesoft/hg/test/Configuration.java Thu Aug 08 19:18:50 2013 +0200 @@ -84,7 +84,7 @@ if (remoteServers == null) { String rr = System.getProperty("hg4j.tests.remote"); assertNotNull("System property hg4j.tests.remote is undefined", rr); - remoteServers = Arrays.asList(rr.split(" ")); + remoteServers = Arrays.asList(rr.split("\\s")); } ArrayList rv = new ArrayList(remoteServers.size()); for (String key : remoteServers) {