blob: 579e6ff796fade7e3516547ebecce493723683de [file] [log] [blame]
/*******************************************************************************
* Copyright (C) 2015, Matthias Sohn <matthias.sohn@sap.com>
* Copyright (C) 2015, Thomas Wolf <thomas.wolf@paranor.ch>
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package org.eclipse.egit.core.op;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.regex.Pattern;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.NetUtil;
import org.eclipse.egit.core.internal.gerrit.GerritUtil;
import org.eclipse.egit.core.op.CloneOperation.PostCloneTask;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.RemoteSession;
import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.io.MessageWriter;
import org.eclipse.jgit.util.io.StreamCopyThread;
/**
* Configure Gerrit if repository was cloned from a Gerrit server
*/
public class ConfigureGerritAfterCloneTask implements PostCloneTask {
private static final String GIT_ECLIPSE_ORG = "git.eclipse.org"; //$NON-NLS-1$
private static final String GERRIT_CONTEXT_ROOT = "/r/"; //$NON-NLS-1$
private static final String HTTP = "http"; //$NON-NLS-1$
private static final String HTTPS = "https"; //$NON-NLS-1$
private static final String SSH = "ssh"; //$NON-NLS-1$
private static final String GERRIT_CONFIG_SERVER_VERSION_API = "/config/server/version"; //$NON-NLS-1$
private static final int GERRIT_SSHD_DEFAULT_PORT = 29418;
private static final String GERRIT_SSHD_VERSION_API = "gerrit version"; //$NON-NLS-1$
/**
* Pattern to match the sshd reply[1] against to determine whether it's a
* Gerrit.
* <p>
* [1]<a href=
* "https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/cmd-version.html">
* Gerrit 2.11 gerrit version ssh command</a>
* </p>
* <p>
* We match the whole reply from Gerrit's sshd (as opposed to a prefix match
* for "gerrit version") just in case a non-Gerrit has the great idea to
* return an error message like "gerrit version: unknown command" or some
* such on its stdout...
* </p>
*/
private static final Pattern GERRIT_SSHD_REPLY = Pattern
.compile(GERRIT_SSHD_VERSION_API
+ "\\s+(?:\\d+(?:\\.\\d+)+|.+-\\d+-g[0-9a-fA-F]{7})"); //$NON-NLS-1$
/**
* To prevent against Cross Site Script Inclusion (XSSI) attacks, the Gerrit
* JSON response body starts with a magic prefix line we can use as a second
* marker beyond the get version endpoint [1] to detect a Gerrit server
* <p/>
* [1] <a href=
* "https://gerrit-documentation.storage.googleapis.com/Documentation/2.11
* /rest-api-config.html#get-version">Gerrit 2.11 Get Version REST
* endpoint</a>
*/
private static final String GERRIT_XSSI_MAGIC_STRING = ")]}\'\n"; //$NON-NLS-1$
private final String uri;
private final String remoteName;
private final CredentialsProvider credentialsProvider;
private int timeout;
/**
* @param uri
* not null
* @param remoteName
* not null
* @param credentialsProvider
* {@link CredentialsProvider} to use for remote communication;
* if {@code null} auto-configuration for repositories cloned
* over ssh will work only for git.eclipse.org
* @param timeout
* timeout for remote communication in seconds
*/
public ConfigureGerritAfterCloneTask(String uri, String remoteName,
CredentialsProvider credentialsProvider, int timeout) {
this.uri = uri;
this.remoteName = remoteName;
this.timeout = timeout;
this.credentialsProvider = credentialsProvider;
}
@Override
public void execute(Repository repository, IProgressMonitor monitor)
throws CoreException {
try {
if (isGerrit(repository)) {
Activator.logInfo(uri
+ " was detected to be hosted by a Gerrit server"); //$NON-NLS-1$
configureGerrit(repository);
}
} catch (Exception e) {
throw new CoreException(Activator.error(e.getMessage(), e));
}
}
/**
* Try to use Gerrit's "Get Version" REST API endpoint [1] to detect if the
* repository is hosted on a Gerrit server.
* <p/>
* [1] <a href=
* "https://gerrit-documentation.storage.googleapis.com/Documentation/2.11
* /rest-api-config.html#get-version">Gerrit 2.11 Get Version REST
* endpoint</a>
*
* @param repo
* the repository to be configured
*
* @return {@code true} if the repository is hosted on a Gerrit server
* @throws IOException
* @throws MalformedURLException
* @throws URISyntaxException
*/
private boolean isGerrit(Repository repo) throws MalformedURLException,
IOException,
URISyntaxException {
URIish u = new URIish(uri);
final String s = u.getScheme();
final String host = u.getHost();
final String path = u.getPath();
// shortcut for Eclipse Gerrit server
if (host != null && host.equals(GIT_ECLIPSE_ORG)) {
if (HTTPS.equals(s) && (u.getPort() == 443 || u.getPort() == -1)
&& path != null && path.startsWith(GERRIT_CONTEXT_ROOT)) {
return true;
} else
if (SSH.equals(s) && u.getPort() == GERRIT_SSHD_DEFAULT_PORT) {
return true;
}
}
if (path != null && (HTTP.equals(s) || HTTPS.equals(s))) {
String baseURL = u.setPath("/").toString(); //$NON-NLS-1$
baseURL = baseURL.substring(0, baseURL.length() - 1);
String tmpPath = ""; //$NON-NLS-1$
HttpURLConnection httpConnection = null;
try {
int slash = 1;
while (true) {
InputStream in = null;
try {
httpConnection = (HttpURLConnection) new URL(baseURL
+ tmpPath + GERRIT_CONFIG_SERVER_VERSION_API)
.openConnection();
NetUtil.setSslVerification(repo, httpConnection);
httpConnection.setRequestMethod("GET"); //$NON-NLS-1$
httpConnection.setReadTimeout(1000 * timeout);
int responseCode = httpConnection.getResponseCode();
switch (responseCode) {
case HttpURLConnection.HTTP_OK:
in = httpConnection.getInputStream();
String response = readFully(in, "UTF-8"); //$NON-NLS-1$
if (response.startsWith(GERRIT_XSSI_MAGIC_STRING)) {
return true;
} else {
return false;
}
case HttpURLConnection.HTTP_NOT_FOUND:
if (slash > path.length()) {
return false;
}
slash = path.indexOf('/', slash);
if (slash == -1) {
// try the entire path
slash = path.length();
}
tmpPath = path.substring(0, slash);
slash++;
break;
default:
return false;
}
} finally {
if (in != null) {
in.close();
}
}
}
} finally {
if (httpConnection != null) {
httpConnection.disconnect();
}
}
} else if (SSH.equals(s)) {
if (u.getPort() < 0 || u.getUser() == null
|| credentialsProvider == null) {
return false;
}
URIish sshUri = u.setPath(""); //$NON-NLS-1$
try {
String result = runSshCommand(sshUri, credentialsProvider,
repo.getFS(), GERRIT_SSHD_VERSION_API);
return result != null
&& GERRIT_SSHD_REPLY.matcher(result).matches();
} catch (IOException e) {
// Something went wrong with the connection or with the command
// execution. Maybe the server didn't recognize the command. Do
// the safe thing and claim it wasn't a Gerrit. In the worst
// case, the user may have to do the Gerrit config setup via
// the ConfigureGerritWizard.
return false;
}
}
return false;
}
private String readFully(InputStream inputStream, String encoding)
throws IOException {
return new String(readFully(inputStream), encoding);
}
private byte[] readFully(InputStream inputStream) throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length = 0;
while ((length = inputStream.read(buffer)) != -1) {
os.write(buffer, 0, length);
}
return os.toByteArray();
}
private void configureGerrit(Repository repository)
throws URISyntaxException, IOException {
StoredConfig config = repository.getConfig();
RemoteConfig remoteConfig;
remoteConfig = GerritUtil.findRemoteConfig(config, remoteName);
if (remoteConfig == null) {
return;
}
GerritUtil.configurePushURI(remoteConfig, new URIish(uri));
GerritUtil.configurePushRefSpec(remoteConfig, Constants.MASTER);
GerritUtil.configureFetchNotes(remoteConfig);
GerritUtil.setCreateChangeId(config);
remoteConfig.update(config);
config.save();
}
private String runSshCommand(URIish sshUri, CredentialsProvider provider,
FS fs, String command) throws IOException {
RemoteSession session = null;
Process process = null;
StreamCopyThread errorThread = null;
try (MessageWriter stderr = new MessageWriter()) {
session = SshSessionFactory.getInstance().getSession(sshUri,
provider, fs, 1000 * timeout);
process = session.exec(command, 0);
errorThread = new StreamCopyThread(process.getErrorStream(),
stderr.getRawStream());
errorThread.start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(),
Constants.CHARSET))) {
return reader.readLine();
}
} finally {
if (errorThread != null) {
try {
errorThread.halt();
} catch (InterruptedException e) {
// Stop waiting and return anyway.
} finally {
errorThread = null;
}
}
if (process != null) {
process.destroy();
}
if (session != null) {
SshSessionFactory.getInstance().releaseSession(session);
}
}
}
}