/*******************************************************************************
* Copyright (c) 2011, 2015 IBM Corporation and others.
* 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
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.orion.server.git.servlets;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryCache;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.orion.internal.server.servlets.file.NewFileServlet;
import org.eclipse.orion.server.core.OrionConfiguration;
import org.eclipse.orion.server.core.PreferenceHelper;
import org.eclipse.orion.server.core.metastore.IMetaStore;
import org.eclipse.orion.server.core.metastore.ProjectInfo;
import org.eclipse.orion.server.core.metastore.WorkspaceInfo;
import org.eclipse.orion.server.git.GitConstants;
import org.eclipse.orion.server.git.GitCredentialsProvider;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GitUtils {
public enum Traverse {
GO_UP, GO_DOWN, CURRENT
}
public static final String KNOWN_GITHUB_HOSTS = "orion.git.knownGithubHosts";
/*
* White list for URL schemes we can allow since they can't be used to gain access to git repositories in another Orion workspace since they require a
* daemon to serve them. Especially file protocol needs to be prohibited (bug 408270).
*/
private static Set<String> uriSchemeWhitelist = new HashSet<String>(Arrays.asList("ftp", "git", "http", "https", "sftp", "ssh"));
/**
* Returns the file representing the Git repository directory for the given file path or any of its parent in the filesystem. If the file doesn't exits, is
* not a Git repository or an error occurred while transforming the given path into a store <code>null</code> is returned.
*
* @param path
* expected format /file/{Workspace}/{projectName}[/{path}]
* @return the .git folder if found or <code>null</code> the give path cannot be resolved to a file or it's not under control of a git repository
* @throws CoreException
*/
public static File getGitDir(IPath path) throws CoreException {
Map<IPath, File> gitDirs = GitUtils.getGitDirs(path, Traverse.GO_UP);
if (gitDirs == null)
return null;
Collection<File> values = gitDirs.values();
if (values.isEmpty())
return null;
return values.toArray(new File[] {})[0];
}
public static File getGitDir(File file) {
if (file.exists()) {
while (file != null) {
File gitDir = resolveGitDir(file);
if (gitDir != null) return gitDir;
file = file.getParentFile();
}
}
return null;
}
/**
* Returns the existing git repositories for the given file path, following the given traversal rule.
*
* @param path
* expected format /file/{Workspace}/{projectName}[/{path}]
* @return a map of all git repositories found, or <code>null</code> if the provided path format doesn't match the expected format.
* @throws CoreException
*/
public static Map<IPath, File> getGitDirs(IPath path, Traverse traverse) throws CoreException {
IPath p = path.removeFirstSegments(1);// remove /file
IFileStore fileStore = NewFileServlet.getFileStore(null, p);
if (fileStore == null)
return null;
Map<IPath, File> result = new HashMap<IPath, File>();
File file = fileStore.toLocalFile(EFS.NONE, null);
// jgit can only handle a local file
if (file == null)
return result;
switch (traverse) {
case CURRENT:
File gitDir = resolveGitDir(file);
if (gitDir != null) {
result.put(new Path(""), gitDir); //$NON-NLS-1$
}
break;
case GO_UP:
getGitDirsInParents(file, result);
break;
case GO_DOWN:
getGitDirsInChildren(file, p, result);
break;
}
return result;
}
private static void getGitDirsInParents(File file, Map<IPath, File> gitDirs) {
int levelUp = 0;
File workspaceRoot = null;
try {
workspaceRoot = OrionConfiguration.getRootLocation().toLocalFile(EFS.NONE, null);
} catch (CoreException e) {
Logger logger = LoggerFactory.getLogger(GitUtils.class);
logger.error("Unable to get the root location", e);
return;
}
if (workspaceRoot == null) {
Logger logger = LoggerFactory.getLogger(GitUtils.class);
logger.error("Unable to get the root location from " + OrionConfiguration.getRootLocation());
return;
}
while (file != null && !file.getAbsolutePath().equals(workspaceRoot.getAbsolutePath())) {
if (file.exists()) {
File gitDir = resolveGitDir(file);
if (gitDir != null && !gitDir.equals(file)) {
gitDirs.put(getPathForLevelUp(levelUp), gitDir);
return;
}
}
file = file.getParentFile();
levelUp++;
}
return;
}
private static IPath getPathForLevelUp(int levelUp) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < levelUp; i++) {
sb.append("../"); //$NON-NLS-1$
}
return new Path(sb.toString());
}
/**
* Recursively walks down a directory tree and collects the paths of all git repositories.
*/
private static void getGitDirsInChildren(File localFile, IPath path, Map<IPath, File> gitDirs) throws CoreException {
if (localFile.exists() && localFile.isDirectory()) {
File gitDir = resolveGitDir(localFile);
if (gitDir != null) {
gitDirs.put(path.addTrailingSeparator(), gitDir);
return;
}
File[] folders = localFile.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory() && !file.getName().equals(Constants.DOT_GIT);
}
});
for (File folder : folders) {
getGitDirsInChildren(folder, path.append(folder.getName()), gitDirs);
}
return;
}
}
public static String getRelativePath(IPath filePath, IPath pathToGitRoot) {
StringBuilder sb = new StringBuilder();
String file = null;
if (!filePath.hasTrailingSeparator()) {
file = filePath.lastSegment();
filePath = filePath.removeLastSegments(1);
}
for (int i = 0; i < pathToGitRoot.segments().length; i++) {
if (pathToGitRoot.segments()[i].equals(".."))
sb.append(filePath.segment(filePath.segments().length - pathToGitRoot.segments().length + i)).append("/");
// else TODO
}
if (file != null)
sb.append(file);
return sb.toString();
}
static GitCredentialsProvider createGitCredentialsProvider(final JSONObject json, HttpServletRequest request) {
String username = json.optString(GitConstants.KEY_USERNAME, null);
char[] password = json.optString(GitConstants.KEY_PASSWORD, "").toCharArray(); //$NON-NLS-1$
String knownHosts = json.optString(GitConstants.KEY_KNOWN_HOSTS, null);
byte[] privateKey = json.optString(GitConstants.KEY_PRIVATE_KEY, "").getBytes(); //$NON-NLS-1$
byte[] publicKey = json.optString(GitConstants.KEY_PUBLIC_KEY, "").getBytes(); //$NON-NLS-1$
byte[] passphrase = json.optString(GitConstants.KEY_PASSPHRASE, "").getBytes(); //$NON-NLS-1$
GitCredentialsProvider cp = new GitCredentialsProvider(null /* set by caller */, request.getRemoteUser(), username, password, knownHosts);
cp.setPrivateKey(privateKey);
cp.setPublicKey(publicKey);
cp.setPassphrase(passphrase);
return cp;
}
public static String encode(String s) {
try {
return URLEncoder.encode(s, "UTF-8"); //$NON-NLS-1$
} catch (UnsupportedEncodingException e) {
// should never happen since "UTF-8" is used
}
return s;
}
public static String decode(String s) {
try {
return URLDecoder.decode(s, "UTF-8"); //$NON-NLS-1$
} catch (UnsupportedEncodingException e) {
// should never happen since "UTF-8" is used
}
return s;
}
/**
* Returns the existing WebProject corresponding to the provided path, or <code>null</code> if no such project exists.
*
* @param path
* path in the form /file/{workspaceId}/{projectName}/[filePath]
* @return the web project, or <code>null</code>
*/
public static ProjectInfo projectFromPath(IPath path) {
if (path == null || path.segmentCount() < 3)
return null;
String workspaceId = path.segment(1);
String projectName = path.segment(2);
try {
return OrionConfiguration.getMetaStore().readProject(workspaceId, projectName);
} catch (CoreException e) {
return null;
}
}
/**
* Returns the HTTP path for the content resource of the given project.
*
* @param workspace
* The web workspace
* @param project
* The web project
* @return the HTTP path of the project content resource
*/
public static IPath pathFromProject(WorkspaceInfo workspace, ProjectInfo project) {
return new Path(org.eclipse.orion.internal.server.servlets.Activator.LOCATION_FILE_SERVLET).append(workspace.getUniqueId()).append(
project.getFullName());
}
/**
* Returns whether or not the git repository URI is forbidden. If a scheme of the URI is matched, check if the scheme is a supported protocol. Otherwise,
* match for a scp-like ssh URI: [user@]host.xz:path/to/repo.git/ and ensure the URI does not represent a local file path.
*
* @param uri
* A git repository URI
* @return a boolean of whether or not the git repository URI is forbidden.
*/
public static boolean isForbiddenGitUri(URIish uri) {
String scheme = uri.getScheme();
String host = uri.getHost();
String path = uri.getPath();
boolean isForbidden = false;
if (scheme != null) {
isForbidden = !uriSchemeWhitelist.contains(scheme);
} else {
// match for a scp-like ssh URI
if (host != null) {
isForbidden = host.length() == 1 || path == null;
} else {
isForbidden = true;
}
}
return isForbidden;
}
/**
* Returns whether the key gerrit.createchangeid is set to true in the git configuration
*
* @param config
* the configuration of the git repository
* @return true if the key gerrit.createchangeid is set to true
*/
public static boolean isGerrit(Config config, String remote) {
String[] list = config.getStringList(ConfigConstants.CONFIG_REMOTE_SECTION, remote, GitConstants.KEY_IS_GERRIT.toLowerCase());
for (int i = 0; i < list.length; i++) {
if (list[i].equals("true")) {
return true;
}
}
return false;
}
public static boolean isInGithub(String url) throws URISyntaxException {
URIish uri = new URIish(url);
String domain = uri.getHost();
if(domain==null){
return false;
}
if(domain.equals("github.com")){
return true;
}
String known = PreferenceHelper.getString(KNOWN_GITHUB_HOSTS);
if(known!=null){
String[] knownHosts = known.split(",");
for(String host : knownHosts){
if(domain.equals(host)){
return true;
}
}
}
return false;
}
public static void _testAllowFileScheme(boolean allow) {
if (allow) {
uriSchemeWhitelist.add("file"); //$NON-NLS-1$
} else {
uriSchemeWhitelist.remove("file"); //$NON-NLS-1$
}
}
public static String getCloneUrl(File gitDir) {
Repository db = null;
try {
db = FileRepositoryBuilder.create(resolveGitDir(gitDir));
return getCloneUrl(db);
} catch (IOException e) {
// ignore and skip Git URL
} finally {
if (db != null) {
db.close();
}
}
return null;
}
/**
* Returns the Git URL for a given git repository.
* @param db
* @return
*/
public static String getCloneUrl(Repository db) {
StoredConfig config = db.getConfig();
return config.getString(ConfigConstants.CONFIG_REMOTE_SECTION, Constants.DEFAULT_REMOTE_NAME, ConfigConstants.CONFIG_KEY_URL);
}
/**
* Returns a unique project name that does not exist in the given workspace, for the given clone name.
*/
public static String getUniqueProjectName(WorkspaceInfo workspace, String cloneName) {
int i = 1;
String uniqueName = cloneName;
IMetaStore store = OrionConfiguration.getMetaStore();
try {
while (store.readProject(workspace.getUniqueId(), uniqueName) != null) {
// add an incrementing counter suffix until we arrive at a unique name
uniqueName = cloneName + '-' + ++i;
}
} catch (CoreException e) {
// let it proceed with current name
}
return uniqueName;
}
/**
* Returns the file representing the Git repository directory for the given file path or any of its parent in the filesystem. If the file doesn't exits, is
* not a Git repository or an error occurred while transforming the given path into a store <code>null</code> is returned.
*
* @param file the file to check
* @return the .git folder if found or <code>null</code> the give path cannot be resolved to a file or it's not under control of a git repository
*/
public static File resolveGitDir(File file) {
File dot = new File(file, Constants.DOT_GIT);
if (RepositoryCache.FileKey.isGitRepository(dot, FS.DETECTED)) {
return dot;
} else if (dot.isFile()) {
try {
return getSymRef(file, dot, FS.DETECTED);
} catch (IOException ignored) {
// Continue searching if gitdir ref isn't found
}
} else if (RepositoryCache.FileKey.isGitRepository(file, FS.DETECTED)) {
return file;
}
return null;
}
public static String sanitizeCookie(String cookieString) {
return cookieString.replaceAll("(\\r|\\n|%0[AaDd])", ""); //$NON-NLS-1$
}
//Note: these helpers are taken from JGit. There is no API in JGit <= 4.1 to resolve sym refs.
private static boolean isSymRef(byte[] ref) {
if (ref.length < 9)
return false;
return /**/ref[0] == 'g' //
&& ref[1] == 'i' //
&& ref[2] == 't' //
&& ref[3] == 'd' //
&& ref[4] == 'i' //
&& ref[5] == 'r' //
&& ref[6] == ':' //
&& ref[7] == ' ';
}
private static File getSymRef(File workTree, File dotGit, FS fs)
throws IOException {
byte[] content = IO.readFully(dotGit);
if (!isSymRef(content))
throw new IOException(MessageFormat.format(
JGitText.get().invalidGitdirRef, dotGit.getAbsolutePath()));
int pathStart = 8;
int lineEnd = RawParseUtils.nextLF(content, pathStart);
if (content[lineEnd - 1] == '\n')
lineEnd--;
if (lineEnd == pathStart)
throw new IOException(MessageFormat.format(
JGitText.get().invalidGitdirRef, dotGit.getAbsolutePath()));
String gitdirPath = RawParseUtils.decode(content, pathStart, lineEnd);
File gitdirFile = fs.resolve(workTree, gitdirPath);
if (gitdirFile.isAbsolute())
return gitdirFile;
else
return new File(workTree, gitdirPath).getCanonicalFile();
}
}