import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.http.server.GitServlet;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.junit.http.AccessEvent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevBlob;
import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.transport.CredentialItem;
+import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.HttpTransport;
import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.transport.http.HttpConnectionFactory;
import org.eclipse.jgit.transport.http.JDKHttpConnectionFactory;
import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory;
-import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.HttpSupport;
-import org.eclipse.jgit.util.SystemReader;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(Parameterized.class)
public class SmartClientSmartServerSslTest extends HttpTestCase {
+ // We run these tests with a server on localhost with a self-signed
+ // certificate. We don't do authentication tests here, so there's no need
+ // for username and password.
+ //
+ // But the server certificate will not validate. We know that Transport will
+ // ask whether we trust the server all the same. This credentials provider
+ // blindly trusts the self-signed certificate by answering "Yes" to all
+ // questions.
+ private CredentialsProvider testCredentials = new CredentialsProvider() {
+
+ @Override
+ public boolean isInteractive() {
+ return false;
+ }
+
+ @Override
+ public boolean supports(CredentialItem... items) {
+ for (CredentialItem item : items) {
+ if (item instanceof CredentialItem.InformationalMessage) {
+ continue;
+ }
+ if (item instanceof CredentialItem.YesNoType) {
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean get(URIish uri, CredentialItem... items)
+ throws UnsupportedCredentialItem {
+ for (CredentialItem item : items) {
+ if (item instanceof CredentialItem.InformationalMessage) {
+ continue;
+ }
+ if (item instanceof CredentialItem.YesNoType) {
+ ((CredentialItem.YesNoType) item).setValue(true);
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+ };
+
private URIish remoteURI;
private URIish secureURI;
src.update(master, B);
src.update("refs/garbage/a/very/long/ref/name/to/compress", B);
-
- FileBasedConfig userConfig = SystemReader.getInstance()
- .openUserConfig(null, FS.DETECTED);
- userConfig.setBoolean("http",
- "https://" + secureURI.getHost() + ':' + server.getSecurePort(),
- "sslVerify", false);
- userConfig.setBoolean("http",
- "http://" + remoteURI.getHost() + ':' + server.getPort(),
- "sslVerify", false);
- userConfig.save();
}
private ServletContextHandler addNormalContext(GitServlet gs, TestRepository<Repository> src, String srcName) {
assertFalse(dst.hasObject(A_txt));
try (Transport t = Transport.open(dst, secureURI)) {
+ t.setCredentialsProvider(testCredentials);
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
}
assertTrue(dst.hasObject(A_txt));
URIish cloneFrom = extendPath(remoteURI, "/https");
try (Transport t = Transport.open(dst, cloneFrom)) {
+ t.setCredentialsProvider(testCredentials);
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
}
assertTrue(dst.hasObject(A_txt));
URIish cloneFrom = extendPath(secureURI, "/back");
try (Transport t = Transport.open(dst, cloneFrom)) {
+ t.setCredentialsProvider(testCredentials);
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
fail("Should have failed (redirect from https to http)");
} catch (TransportException e) {
}
}
+ @Test
+ public void testInitialClone_SslFailure() throws Exception {
+ Repository dst = createBareRepository();
+ assertFalse(dst.hasObject(A_txt));
+
+ try (Transport t = Transport.open(dst, secureURI)) {
+ // Set a credentials provider that doesn't handle questions
+ t.setCredentialsProvider(
+ new UsernamePasswordCredentialsProvider("any", "anypwd"));
+ t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+ fail("Should have failed (SSL certificate not trusted)");
+ } catch (TransportException e) {
+ assertTrue(e.getMessage().contains("Secure connection"));
+ }
+ }
+
}
import java.net.ProxySelector;
import java.net.URISyntaxException;
import java.net.URL;
+import java.security.cert.CertPathBuilderException;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.CertificateException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
+import javax.net.ssl.SSLHandshakeException;
+
import org.eclipse.jgit.errors.NoRemoteRepositoryException;
import org.eclipse.jgit.errors.NotSupportedException;
import org.eclipse.jgit.errors.PackProtocolException;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.lib.SymbolicRef;
import org.eclipse.jgit.transport.HttpAuthMethod.Type;
import org.eclipse.jgit.transport.HttpConfig.HttpRedirectMode;
import org.eclipse.jgit.transport.http.HttpConnection;
+import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.HttpSupport;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.SystemReader;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.eclipse.jgit.util.io.UnionInputStream;
private URL objectsUrl;
- final HttpConfig http;
+ private final HttpConfig http;
private final ProxySelector proxySelector;
private Map<String, String> headers;
+ private boolean sslVerify;
+
+ private boolean sslFailure = false;
+
TransportHttp(final Repository local, final URIish uri)
throws NotSupportedException {
super(local, uri);
setURI(uri);
http = new HttpConfig(local.getConfig(), uri);
proxySelector = ProxySelector.getDefault();
+ sslVerify = http.isSslVerify();
}
private URL toURL(URIish urish) throws MalformedURLException {
setURI(uri);
http = new HttpConfig(uri);
proxySelector = ProxySelector.getDefault();
+ sslVerify = http.isSslVerify();
}
/**
throw e;
} catch (TransportException e) {
throw e;
+ } catch (SSLHandshakeException e) {
+ handleSslFailure(e);
+ continue; // Re-try
} catch (IOException e) {
if (authMethod.getType() != HttpAuthMethod.Type.NONE) {
if (ignoreTypes == null) {
}
}
+ private static class CredentialItems {
+ CredentialItem.InformationalMessage message;
+
+ /** Trust the server for this git operation */
+ CredentialItem.YesNoType now;
+
+ /**
+ * Trust the server for all git operations from this repository; may be
+ * {@code null} if the transport was created via
+ * {@link #TransportHttp(URIish)}.
+ */
+ CredentialItem.YesNoType forRepo;
+
+ /** Always trust the server from now on. */
+ CredentialItem.YesNoType always;
+
+ public CredentialItem[] items() {
+ if (forRepo == null) {
+ return new CredentialItem[] { message, now, always };
+ } else {
+ return new CredentialItem[] { message, now, forRepo, always };
+ }
+ }
+ }
+
+ private void handleSslFailure(Throwable e) throws TransportException {
+ if (sslFailure || !trustInsecureSslConnection(e.getCause())) {
+ throw new TransportException(uri,
+ MessageFormat.format(
+ JGitText.get().sslFailureExceptionMessage,
+ currentUri.setPass(null)),
+ e);
+ }
+ sslFailure = true;
+ }
+
+ private boolean trustInsecureSslConnection(Throwable cause) {
+ if (cause instanceof CertificateException
+ || cause instanceof CertPathBuilderException
+ || cause instanceof CertPathValidatorException) {
+ // Certificate expired or revoked, PKIX path building not
+ // possible, self-signed certificate, host does not match ...
+ CredentialsProvider provider = getCredentialsProvider();
+ if (provider != null) {
+ CredentialItems trust = constructSslTrustItems(cause);
+ CredentialItem[] items = trust.items();
+ if (provider.supports(items)) {
+ boolean answered = provider.get(uri, items);
+ if (answered) {
+ // Not canceled
+ boolean trustNow = trust.now.getValue();
+ boolean trustLocal = trust.forRepo != null
+ && trust.forRepo.getValue();
+ boolean trustAlways = trust.always.getValue();
+ if (trustNow || trustLocal || trustAlways) {
+ sslVerify = false;
+ if (trustAlways) {
+ updateSslVerify(SystemReader.getInstance()
+ .openUserConfig(null, FS.DETECTED),
+ false);
+ } else if (trustLocal) {
+ updateSslVerify(local.getConfig(), false);
+ }
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private CredentialItems constructSslTrustItems(Throwable cause) {
+ CredentialItems items = new CredentialItems();
+ String info = MessageFormat.format(JGitText.get().sslFailureInfo,
+ currentUri.setPass(null));
+ String sslMessage = cause.getLocalizedMessage();
+ if (sslMessage == null) {
+ sslMessage = cause.toString();
+ }
+ sslMessage = MessageFormat.format(JGitText.get().sslFailureCause,
+ sslMessage);
+ items.message = new CredentialItem.InformationalMessage(info + '\n'
+ + sslMessage + '\n'
+ + JGitText.get().sslFailureTrustExplanation);
+ items.now = new CredentialItem.YesNoType(JGitText.get().sslTrustNow);
+ if (local != null) {
+ items.forRepo = new CredentialItem.YesNoType(
+ MessageFormat.format(JGitText.get().sslTrustForRepo,
+ local.getDirectory()));
+ }
+ items.always = new CredentialItem.YesNoType(
+ JGitText.get().sslTrustAlways);
+ return items;
+ }
+
+ private void updateSslVerify(StoredConfig config, boolean value) {
+ // Since git uses the original URI for matching, we must also use the
+ // original URI and cannot use the current URI (which might be different
+ // after redirects)
+ String uriPattern = uri.getScheme() + "://" + uri.getHost(); //$NON-NLS-1$
+ int port = uri.getPort();
+ if (port > 0) {
+ uriPattern += ":" + port; //$NON-NLS-1$
+ }
+ config.setBoolean(HttpConfig.HTTP, uriPattern,
+ HttpConfig.SSL_VERIFY_KEY, value);
+ try {
+ config.save();
+ } catch (IOException e) {
+ LOG.error(JGitText.get().sslVerifyCannotSave, e);
+ }
+ }
+
private URIish redirect(String location, String checkFor, int redirects)
throws TransportException {
if (location == null || location.isEmpty()) {
final Proxy proxy = HttpSupport.proxyFor(proxySelector, u);
HttpConnection conn = connectionFactory.create(u, proxy);
- if (!http.isSslVerify() && "https".equals(u.getProtocol())) { //$NON-NLS-1$
+ if (!sslVerify && "https".equals(u.getProtocol())) { //$NON-NLS-1$
HttpSupport.disableSslVerify(conn);
}
int authAttempts = 1;
int redirects = 0;
for (;;) {
- // The very first time we will try with the authentication
- // method used on the initial GET request. This is a hint only;
- // it may fail. If so, we'll then re-try with proper 401
- // handling, going through the available authentication schemes.
- openStream();
- if (buf != out) {
- conn.setRequestProperty(HDR_CONTENT_ENCODING, ENCODING_GZIP);
- }
- conn.setFixedLengthStreamingMode((int) buf.length());
- try (OutputStream httpOut = conn.getOutputStream()) {
- buf.writeTo(httpOut, null);
- }
-
- final int status = HttpSupport.response(conn);
- switch (status) {
- case HttpConnection.HTTP_OK:
- // We're done.
- return;
-
- case HttpConnection.HTTP_NOT_FOUND:
- throw new NoRemoteRepositoryException(uri, MessageFormat
- .format(JGitText.get().uriNotFound, conn.getURL()));
-
- case HttpConnection.HTTP_FORBIDDEN:
- throw new TransportException(uri,
- MessageFormat.format(
- JGitText.get().serviceNotPermitted,
- baseUrl, serviceName));
-
- case HttpConnection.HTTP_MOVED_PERM:
- case HttpConnection.HTTP_MOVED_TEMP:
- case HttpConnection.HTTP_11_MOVED_TEMP:
- // SEE_OTHER after a POST doesn't make sense for a git
- // server, so we don't handle it here and thus we'll
- // report an error in openResponse() later on.
- if (http.getFollowRedirects() != HttpRedirectMode.TRUE) {
- // Let openResponse() issue an error
- return;
+ try {
+ // The very first time we will try with the authentication
+ // method used on the initial GET request. This is a hint
+ // only; it may fail. If so, we'll then re-try with proper
+ // 401 handling, going through the available authentication
+ // schemes.
+ openStream();
+ if (buf != out) {
+ conn.setRequestProperty(HDR_CONTENT_ENCODING,
+ ENCODING_GZIP);
}
- currentUri = redirect(
- conn.getHeaderField(HDR_LOCATION),
- '/' + serviceName, redirects++);
- try {
- baseUrl = toURL(currentUri);
- } catch (MalformedURLException e) {
- throw new TransportException(uri, MessageFormat.format(
- JGitText.get().invalidRedirectLocation,
- baseUrl, currentUri), e);
+ conn.setFixedLengthStreamingMode((int) buf.length());
+ try (OutputStream httpOut = conn.getOutputStream()) {
+ buf.writeTo(httpOut, null);
}
- continue;
- case HttpConnection.HTTP_UNAUTHORIZED:
- HttpAuthMethod nextMethod = HttpAuthMethod
- .scanResponse(conn, ignoreTypes);
- switch (nextMethod.getType()) {
- case NONE:
+ final int status = HttpSupport.response(conn);
+ switch (status) {
+ case HttpConnection.HTTP_OK:
+ // We're done.
+ return;
+
+ case HttpConnection.HTTP_NOT_FOUND:
+ throw new NoRemoteRepositoryException(uri,
+ MessageFormat.format(JGitText.get().uriNotFound,
+ conn.getURL()));
+
+ case HttpConnection.HTTP_FORBIDDEN:
throw new TransportException(uri,
MessageFormat.format(
- JGitText.get().authenticationNotSupported,
- conn.getURL()));
- case NEGOTIATE:
- // RFC 4559 states "When using the SPNEGO [...] with
- // [...] POST, the authentication should be complete
- // [...] before sending the user data." So in theory
- // the initial GET should have been authenticated
- // already. (Unless there was a redirect?)
- //
- // We try this only once:
- ignoreTypes.add(HttpAuthMethod.Type.NEGOTIATE);
- if (authenticator != null) {
- ignoreTypes.add(authenticator.getType());
+ JGitText.get().serviceNotPermitted,
+ baseUrl, serviceName));
+
+ case HttpConnection.HTTP_MOVED_PERM:
+ case HttpConnection.HTTP_MOVED_TEMP:
+ case HttpConnection.HTTP_11_MOVED_TEMP:
+ // SEE_OTHER after a POST doesn't make sense for a git
+ // server, so we don't handle it here and thus we'll
+ // report an error in openResponse() later on.
+ if (http.getFollowRedirects() != HttpRedirectMode.TRUE) {
+ // Let openResponse() issue an error
+ return;
}
- authAttempts = 1;
- // We only do the Kerberos part of SPNEGO, which
- // requires only one attempt. We do *not* to the
- // NTLM part of SPNEGO; it's a multi-round
- // negotiation and among other problems it would
- // be unclear when to stop if no HTTP_OK is
- // forthcoming. In theory a malicious server
- // could keep sending requests for another NTLM
- // round, keeping a client stuck here.
- break;
- default:
- // DIGEST or BASIC. Let's be sure we ignore NEGOTIATE;
- // if it was available, we have tried it before.
- ignoreTypes.add(HttpAuthMethod.Type.NEGOTIATE);
- if (authenticator == null || authenticator
- .getType() != nextMethod.getType()) {
+ currentUri = redirect(conn.getHeaderField(HDR_LOCATION),
+ '/' + serviceName, redirects++);
+ try {
+ baseUrl = toURL(currentUri);
+ } catch (MalformedURLException e) {
+ throw new TransportException(uri,
+ MessageFormat.format(
+ JGitText.get().invalidRedirectLocation,
+ baseUrl, currentUri),
+ e);
+ }
+ continue;
+
+ case HttpConnection.HTTP_UNAUTHORIZED:
+ HttpAuthMethod nextMethod = HttpAuthMethod
+ .scanResponse(conn, ignoreTypes);
+ switch (nextMethod.getType()) {
+ case NONE:
+ throw new TransportException(uri,
+ MessageFormat.format(
+ JGitText.get().authenticationNotSupported,
+ conn.getURL()));
+ case NEGOTIATE:
+ // RFC 4559 states "When using the SPNEGO [...] with
+ // [...] POST, the authentication should be complete
+ // [...] before sending the user data." So in theory
+ // the initial GET should have been authenticated
+ // already. (Unless there was a redirect?)
+ //
+ // We try this only once:
+ ignoreTypes.add(HttpAuthMethod.Type.NEGOTIATE);
if (authenticator != null) {
ignoreTypes.add(authenticator.getType());
}
authAttempts = 1;
+ // We only do the Kerberos part of SPNEGO, which
+ // requires only one attempt. We do *not* to the
+ // NTLM part of SPNEGO; it's a multi-round
+ // negotiation and among other problems it would
+ // be unclear when to stop if no HTTP_OK is
+ // forthcoming. In theory a malicious server
+ // could keep sending requests for another NTLM
+ // round, keeping a client stuck here.
+ break;
+ default:
+ // DIGEST or BASIC. Let's be sure we ignore
+ // NEGOTIATE; if it was available, we have tried it
+ // before.
+ ignoreTypes.add(HttpAuthMethod.Type.NEGOTIATE);
+ if (authenticator == null || authenticator
+ .getType() != nextMethod.getType()) {
+ if (authenticator != null) {
+ ignoreTypes.add(authenticator.getType());
+ }
+ authAttempts = 1;
+ }
+ break;
}
- break;
- }
- authMethod = nextMethod;
- authenticator = nextMethod;
- CredentialsProvider credentialsProvider = getCredentialsProvider();
- if (credentialsProvider == null) {
- throw new TransportException(uri,
- JGitText.get().noCredentialsProvider);
- }
- if (authAttempts > 1) {
- credentialsProvider.reset(currentUri);
- }
- if (3 < authAttempts || !authMethod.authorize(currentUri,
- credentialsProvider)) {
- throw new TransportException(uri,
- JGitText.get().notAuthorized);
- }
- authAttempts++;
- continue;
+ authMethod = nextMethod;
+ authenticator = nextMethod;
+ CredentialsProvider credentialsProvider = getCredentialsProvider();
+ if (credentialsProvider == null) {
+ throw new TransportException(uri,
+ JGitText.get().noCredentialsProvider);
+ }
+ if (authAttempts > 1) {
+ credentialsProvider.reset(currentUri);
+ }
+ if (3 < authAttempts || !authMethod
+ .authorize(currentUri, credentialsProvider)) {
+ throw new TransportException(uri,
+ JGitText.get().notAuthorized);
+ }
+ authAttempts++;
+ continue;
- default:
- // Just return here; openResponse() will report an appropriate
- // error.
- return;
+ default:
+ // Just return here; openResponse() will report an
+ // appropriate error.
+ return;
+ }
+ } catch (SSLHandshakeException e) {
+ handleSslFailure(e);
+ continue; // Re-try
}
}
}