/*
 * Copyright (C) 2008-2010, Google Inc.
 * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package org.eclipse.jgit.transport;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.eclipse.jgit.errors.NoRemoteRepositoryException;
import org.eclipse.jgit.errors.NotSupportedException;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.QuotedString;
import org.eclipse.jgit.util.SystemReader;
import org.eclipse.jgit.util.io.MessageWriter;
import org.eclipse.jgit.util.io.StreamCopyThread;

Transport through an SSH tunnel.

The SSH transport requires the remote side to have Git installed, as the transport logs into the remote system and executes a Git helper program on the remote side to read (or write) the remote repository's files.

This transport does not support direct SCP style of copying files, as it assumes there are Git specific smarts on the remote side to perform object enumeration, save file modification and hook execution.

/** * Transport through an SSH tunnel. * <p> * The SSH transport requires the remote side to have Git installed, as the * transport logs into the remote system and executes a Git helper program on * the remote side to read (or write) the remote repository's files. * <p> * This transport does not support direct SCP style of copying files, as it * assumes there are Git specific smarts on the remote side to perform object * enumeration, save file modification and hook execution. */
public class TransportGitSsh extends SshTransport implements PackTransport { private static final String EXT = "ext"; //$NON-NLS-1$ static final TransportProtocol PROTO_SSH = new TransportProtocol() { private final String[] schemeNames = { "ssh", "ssh+git", "git+ssh" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ private final Set<String> schemeSet = Collections .unmodifiableSet(new LinkedHashSet<>(Arrays .asList(schemeNames))); @Override public String getName() { return JGitText.get().transportProtoSSH; } @Override public Set<String> getSchemes() { return schemeSet; } @Override public Set<URIishField> getRequiredFields() { return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST, URIishField.PATH)); } @Override public Set<URIishField> getOptionalFields() { return Collections.unmodifiableSet(EnumSet.of(URIishField.USER, URIishField.PASS, URIishField.PORT)); } @Override public int getDefaultPort() { return 22; } @Override public boolean canHandle(URIish uri, Repository local, String remoteName) { if (uri.getScheme() == null) { // scp-style URI "host:path" does not have scheme. return uri.getHost() != null && uri.getPath() != null && uri.getHost().length() != 0 && uri.getPath().length() != 0; } return super.canHandle(uri, local, remoteName); } @Override public Transport open(URIish uri, Repository local, String remoteName) throws NotSupportedException { return new TransportGitSsh(local, uri); } @Override public Transport open(URIish uri) throws NotSupportedException, TransportException { return new TransportGitSsh(uri); } }; TransportGitSsh(Repository local, URIish uri) { super(local, uri); initSshSessionFactory(); } TransportGitSsh(URIish uri) { super(uri); initSshSessionFactory(); } private void initSshSessionFactory() { if (useExtSession()) { setSshSessionFactory(new SshSessionFactory() { @Override public RemoteSession getSession(URIish uri2, CredentialsProvider credentialsProvider, FS fs, int tms) throws TransportException { return new ExtSession(); } @Override public String getType() { return EXT; } }); } }
{@inheritDoc}
/** {@inheritDoc} */
@Override public FetchConnection openFetch() throws TransportException { return new SshFetchConnection(); }
{@inheritDoc}
/** {@inheritDoc} */
@Override public PushConnection openPush() throws TransportException { return new SshPushConnection(); } String commandFor(String exe) { String path = uri.getPath(); if (uri.getScheme() != null && uri.getPath().startsWith("/~")) //$NON-NLS-1$ path = (uri.getPath().substring(1)); final StringBuilder cmd = new StringBuilder(); cmd.append(exe); cmd.append(' '); cmd.append(QuotedString.BOURNE.quote(path)); return cmd.toString(); } void checkExecFailure(int status, String exe, String why) throws TransportException { if (status == 127) { IOException cause = null; if (why != null && why.length() > 0) cause = new IOException(why); throw new TransportException(uri, MessageFormat.format( JGitText.get().cannotExecute, commandFor(exe)), cause); } } NoRemoteRepositoryException cleanNotFound(NoRemoteRepositoryException nf, String why) { if (why == null || why.length() == 0) return nf; String path = uri.getPath(); if (uri.getScheme() != null && uri.getPath().startsWith("/~")) //$NON-NLS-1$ path = uri.getPath().substring(1); final StringBuilder pfx = new StringBuilder(); pfx.append("fatal: "); //$NON-NLS-1$ pfx.append(QuotedString.BOURNE.quote(path)); pfx.append(": "); //$NON-NLS-1$ if (why.startsWith(pfx.toString())) why = why.substring(pfx.length()); return new NoRemoteRepositoryException(uri, why); } private static boolean useExtSession() { return SystemReader.getInstance().getenv("GIT_SSH") != null; //$NON-NLS-1$ } private class ExtSession implements RemoteSession { @Override public Process exec(String command, int timeout) throws TransportException { String ssh = SystemReader.getInstance().getenv("GIT_SSH"); //$NON-NLS-1$ boolean putty = ssh.toLowerCase(Locale.ROOT).contains("plink"); //$NON-NLS-1$ List<String> args = new ArrayList<>(); args.add(ssh); if (putty && !ssh.toLowerCase(Locale.ROOT).contains("tortoiseplink")) //$NON-NLS-1$ args.add("-batch"); //$NON-NLS-1$ if (0 < getURI().getPort()) { args.add(putty ? "-P" : "-p"); //$NON-NLS-1$ //$NON-NLS-2$ args.add(String.valueOf(getURI().getPort())); } if (getURI().getUser() != null) args.add(getURI().getUser() + "@" + getURI().getHost()); //$NON-NLS-1$ else args.add(getURI().getHost()); args.add(command); ProcessBuilder pb = createProcess(args); try { return pb.start(); } catch (IOException err) { throw new TransportException(err.getMessage(), err); } } private ProcessBuilder createProcess(List<String> args) { ProcessBuilder pb = new ProcessBuilder(); pb.command(args); File directory = local != null ? local.getDirectory() : null; if (directory != null) { pb.environment().put(Constants.GIT_DIR_KEY, directory.getPath()); } return pb; } @Override public void disconnect() { // Nothing to do } } class SshFetchConnection extends BasePackFetchConnection { private final Process process; private StreamCopyThread errorThread; SshFetchConnection() throws TransportException { super(TransportGitSsh.this); try { process = getSession().exec(commandFor(getOptionUploadPack()), getTimeout()); final MessageWriter msg = new MessageWriter(); setMessageWriter(msg); final InputStream upErr = process.getErrorStream(); errorThread = new StreamCopyThread(upErr, msg.getRawStream()); errorThread.start(); init(process.getInputStream(), process.getOutputStream()); } catch (TransportException err) { close(); throw err; } catch (Throwable err) { close(); throw new TransportException(uri, JGitText.get().remoteHungUpUnexpectedly, err); } try { readAdvertisedRefs(); } catch (NoRemoteRepositoryException notFound) { final String msgs = getMessages(); checkExecFailure(process.exitValue(), getOptionUploadPack(), msgs); throw cleanNotFound(notFound, msgs); } } @Override public void close() { endOut(); if (process != null) { process.destroy(); } if (errorThread != null) { try { errorThread.halt(); } catch (InterruptedException e) { // Stop waiting and return anyway. } finally { errorThread = null; } } super.close(); } } class SshPushConnection extends BasePackPushConnection { private final Process process; private StreamCopyThread errorThread; SshPushConnection() throws TransportException { super(TransportGitSsh.this); try { process = getSession().exec(commandFor(getOptionReceivePack()), getTimeout()); final MessageWriter msg = new MessageWriter(); setMessageWriter(msg); final InputStream rpErr = process.getErrorStream(); errorThread = new StreamCopyThread(rpErr, msg.getRawStream()); errorThread.start(); init(process.getInputStream(), process.getOutputStream()); } catch (TransportException err) { try { close(); } catch (Exception e) { // ignore } throw err; } catch (Throwable err) { try { close(); } catch (Exception e) { // ignore } throw new TransportException(uri, JGitText.get().remoteHungUpUnexpectedly, err); } try { readAdvertisedRefs(); } catch (NoRemoteRepositoryException notFound) { final String msgs = getMessages(); checkExecFailure(process.exitValue(), getOptionReceivePack(), msgs); throw cleanNotFound(notFound, msgs); } } @Override public void close() { endOut(); if (process != null) { process.destroy(); } if (errorThread != null) { try { errorThread.halt(); } catch (InterruptedException e) { // Stop waiting and return anyway. } finally { errorThread = null; } } super.close(); } } }