TransportGitSsh.java

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

  13. package org.eclipse.jgit.transport;

  14. import java.io.File;
  15. import java.io.IOException;
  16. import java.io.InputStream;
  17. import java.text.MessageFormat;
  18. import java.util.ArrayList;
  19. import java.util.Arrays;
  20. import java.util.Collection;
  21. import java.util.Collections;
  22. import java.util.EnumSet;
  23. import java.util.LinkedHashSet;
  24. import java.util.List;
  25. import java.util.Locale;
  26. import java.util.Map;
  27. import java.util.Set;

  28. import org.eclipse.jgit.errors.NoRemoteRepositoryException;
  29. import org.eclipse.jgit.errors.NotSupportedException;
  30. import org.eclipse.jgit.errors.TransportException;
  31. import org.eclipse.jgit.internal.JGitText;
  32. import org.eclipse.jgit.lib.Constants;
  33. import org.eclipse.jgit.lib.Repository;
  34. import org.eclipse.jgit.util.FS;
  35. import org.eclipse.jgit.util.QuotedString;
  36. import org.eclipse.jgit.util.SystemReader;
  37. import org.eclipse.jgit.util.io.MessageWriter;
  38. import org.eclipse.jgit.util.io.StreamCopyThread;

  39. /**
  40.  * Transport through an SSH tunnel.
  41.  * <p>
  42.  * The SSH transport requires the remote side to have Git installed, as the
  43.  * transport logs into the remote system and executes a Git helper program on
  44.  * the remote side to read (or write) the remote repository's files.
  45.  * <p>
  46.  * This transport does not support direct SCP style of copying files, as it
  47.  * assumes there are Git specific smarts on the remote side to perform object
  48.  * enumeration, save file modification and hook execution.
  49.  */
  50. public class TransportGitSsh extends SshTransport implements PackTransport {
  51.     private static final String EXT = "ext"; //$NON-NLS-1$

  52.     static final TransportProtocol PROTO_SSH = new TransportProtocol() {
  53.         private final String[] schemeNames = { "ssh", "ssh+git", "git+ssh" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

  54.         private final Set<String> schemeSet = Collections
  55.                 .unmodifiableSet(new LinkedHashSet<>(Arrays
  56.                         .asList(schemeNames)));

  57.         @Override
  58.         public String getName() {
  59.             return JGitText.get().transportProtoSSH;
  60.         }

  61.         @Override
  62.         public Set<String> getSchemes() {
  63.             return schemeSet;
  64.         }

  65.         @Override
  66.         public Set<URIishField> getRequiredFields() {
  67.             return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST,
  68.                     URIishField.PATH));
  69.         }

  70.         @Override
  71.         public Set<URIishField> getOptionalFields() {
  72.             return Collections.unmodifiableSet(EnumSet.of(URIishField.USER,
  73.                     URIishField.PASS, URIishField.PORT));
  74.         }

  75.         @Override
  76.         public int getDefaultPort() {
  77.             return 22;
  78.         }

  79.         @Override
  80.         public boolean canHandle(URIish uri, Repository local, String remoteName) {
  81.             if (uri.getScheme() == null) {
  82.                 // scp-style URI "host:path" does not have scheme.
  83.                 return uri.getHost() != null
  84.                     && uri.getPath() != null
  85.                     && uri.getHost().length() != 0
  86.                     && uri.getPath().length() != 0;
  87.             }
  88.             return super.canHandle(uri, local, remoteName);
  89.         }

  90.         @Override
  91.         public Transport open(URIish uri, Repository local, String remoteName)
  92.                 throws NotSupportedException {
  93.             return new TransportGitSsh(local, uri);
  94.         }

  95.         @Override
  96.         public Transport open(URIish uri) throws NotSupportedException, TransportException {
  97.             return new TransportGitSsh(uri);
  98.         }
  99.     };

  100.     TransportGitSsh(Repository local, URIish uri) {
  101.         super(local, uri);
  102.         initSshSessionFactory();
  103.     }

  104.     TransportGitSsh(URIish uri) {
  105.         super(uri);
  106.         initSshSessionFactory();
  107.     }

  108.     private void initSshSessionFactory() {
  109.         if (useExtSession()) {
  110.             setSshSessionFactory(new SshSessionFactory() {
  111.                 @Override
  112.                 public RemoteSession getSession(URIish uri2,
  113.                         CredentialsProvider credentialsProvider, FS fs, int tms)
  114.                         throws TransportException {
  115.                     return new ExtSession();
  116.                 }

  117.                 @Override
  118.                 public String getType() {
  119.                     return EXT;
  120.                 }
  121.             });
  122.         }
  123.     }

  124.     /** {@inheritDoc} */
  125.     @Override
  126.     public FetchConnection openFetch() throws TransportException {
  127.         return new SshFetchConnection();
  128.     }

  129.     @Override
  130.     public FetchConnection openFetch(Collection<RefSpec> refSpecs,
  131.             String... additionalPatterns)
  132.             throws NotSupportedException, TransportException {
  133.         return new SshFetchConnection(refSpecs, additionalPatterns);
  134.     }

  135.     /** {@inheritDoc} */
  136.     @Override
  137.     public PushConnection openPush() throws TransportException {
  138.         return new SshPushConnection();
  139.     }

  140.     String commandFor(String exe) {
  141.         String path = uri.getPath();
  142.         if (uri.getScheme() != null && uri.getPath().startsWith("/~")) //$NON-NLS-1$
  143.             path = (uri.getPath().substring(1));

  144.         final StringBuilder cmd = new StringBuilder();
  145.         cmd.append(exe);
  146.         cmd.append(' ');
  147.         cmd.append(QuotedString.BOURNE.quote(path));
  148.         return cmd.toString();
  149.     }

  150.     void checkExecFailure(int status, String exe, String why)
  151.             throws TransportException {
  152.         if (status == 127) {
  153.             IOException cause = null;
  154.             if (why != null && why.length() > 0)
  155.                 cause = new IOException(why);
  156.             throw new TransportException(uri, MessageFormat.format(
  157.                     JGitText.get().cannotExecute, commandFor(exe)), cause);
  158.         }
  159.     }

  160.     NoRemoteRepositoryException cleanNotFound(NoRemoteRepositoryException nf,
  161.             String why) {
  162.         if (why == null || why.length() == 0)
  163.             return nf;

  164.         String path = uri.getPath();
  165.         if (uri.getScheme() != null && uri.getPath().startsWith("/~")) //$NON-NLS-1$
  166.             path = uri.getPath().substring(1);

  167.         final StringBuilder pfx = new StringBuilder();
  168.         pfx.append("fatal: "); //$NON-NLS-1$
  169.         pfx.append(QuotedString.BOURNE.quote(path));
  170.         pfx.append(": "); //$NON-NLS-1$
  171.         if (why.startsWith(pfx.toString()))
  172.             why = why.substring(pfx.length());

  173.         return new NoRemoteRepositoryException(uri, why);
  174.     }

  175.     private static boolean useExtSession() {
  176.         return SystemReader.getInstance().getenv("GIT_SSH") != null; //$NON-NLS-1$
  177.     }

  178.     private class ExtSession implements RemoteSession2 {

  179.         @Override
  180.         public Process exec(String command, int timeout)
  181.                 throws TransportException {
  182.             return exec(command, null, timeout);
  183.         }

  184.         @Override
  185.         public Process exec(String command, Map<String, String> environment,
  186.                 int timeout) throws TransportException {
  187.             String ssh = SystemReader.getInstance().getenv("GIT_SSH"); //$NON-NLS-1$
  188.             boolean putty = ssh.toLowerCase(Locale.ROOT).contains("plink"); //$NON-NLS-1$

  189.             List<String> args = new ArrayList<>();
  190.             args.add(ssh);
  191.             if (putty && !ssh.toLowerCase(Locale.ROOT)
  192.                     .contains("tortoiseplink")) {//$NON-NLS-1$
  193.                 args.add("-batch"); //$NON-NLS-1$
  194.             }
  195.             if (0 < getURI().getPort()) {
  196.                 args.add(putty ? "-P" : "-p"); //$NON-NLS-1$ //$NON-NLS-2$
  197.                 args.add(String.valueOf(getURI().getPort()));
  198.             }
  199.             if (getURI().getUser() != null) {
  200.                 args.add(getURI().getUser() + "@" + getURI().getHost()); //$NON-NLS-1$
  201.             } else {
  202.                 args.add(getURI().getHost());
  203.             }
  204.             args.add(command);

  205.             ProcessBuilder pb = createProcess(args, environment);
  206.             try {
  207.                 return pb.start();
  208.             } catch (IOException err) {
  209.                 throw new TransportException(err.getMessage(), err);
  210.             }
  211.         }

  212.         private ProcessBuilder createProcess(List<String> args,
  213.                 Map<String, String> environment) {
  214.             ProcessBuilder pb = new ProcessBuilder();
  215.             pb.command(args);
  216.             if (environment != null) {
  217.                 pb.environment().putAll(environment);
  218.             }
  219.             File directory = local != null ? local.getDirectory() : null;
  220.             if (directory != null) {
  221.                 pb.environment().put(Constants.GIT_DIR_KEY,
  222.                         directory.getPath());
  223.             }
  224.             return pb;
  225.         }

  226.         @Override
  227.         public void disconnect() {
  228.             // Nothing to do
  229.         }
  230.     }

  231.     class SshFetchConnection extends BasePackFetchConnection {
  232.         private final Process process;

  233.         private StreamCopyThread errorThread;

  234.         SshFetchConnection() throws TransportException {
  235.             this(Collections.emptyList());
  236.         }

  237.         SshFetchConnection(Collection<RefSpec> refSpecs,
  238.                 String... additionalPatterns) throws TransportException {
  239.             super(TransportGitSsh.this);
  240.             try {
  241.                 RemoteSession session = getSession();
  242.                 TransferConfig.ProtocolVersion gitProtocol = protocol;
  243.                 if (gitProtocol == null) {
  244.                     gitProtocol = TransferConfig.ProtocolVersion.V2;
  245.                 }
  246.                 if (session instanceof RemoteSession2
  247.                         && TransferConfig.ProtocolVersion.V2
  248.                                 .equals(gitProtocol)) {
  249.                     process = ((RemoteSession2) session).exec(
  250.                             commandFor(getOptionUploadPack()), Collections
  251.                                     .singletonMap(
  252.                                             GitProtocolConstants.PROTOCOL_ENVIRONMENT_VARIABLE,
  253.                                             GitProtocolConstants.VERSION_2_REQUEST),
  254.                             getTimeout());
  255.                 } else {
  256.                     process = session.exec(commandFor(getOptionUploadPack()),
  257.                             getTimeout());
  258.                 }
  259.                 final MessageWriter msg = new MessageWriter();
  260.                 setMessageWriter(msg);

  261.                 final InputStream upErr = process.getErrorStream();
  262.                 errorThread = new StreamCopyThread(upErr, msg.getRawStream());
  263.                 errorThread.start();

  264.                 init(process.getInputStream(), process.getOutputStream());

  265.             } catch (TransportException err) {
  266.                 close();
  267.                 throw err;
  268.             } catch (Throwable err) {
  269.                 close();
  270.                 throw new TransportException(uri,
  271.                         JGitText.get().remoteHungUpUnexpectedly, err);
  272.             }

  273.             try {
  274.                 if (!readAdvertisedRefs()) {
  275.                     lsRefs(refSpecs, additionalPatterns);
  276.                 }
  277.             } catch (NoRemoteRepositoryException notFound) {
  278.                 final String msgs = getMessages();
  279.                 checkExecFailure(process.exitValue(), getOptionUploadPack(),
  280.                         msgs);
  281.                 throw cleanNotFound(notFound, msgs);
  282.             }
  283.         }

  284.         @Override
  285.         public void close() {
  286.             endOut();

  287.             if (process != null) {
  288.                 process.destroy();
  289.             }
  290.             if (errorThread != null) {
  291.                 try {
  292.                     errorThread.halt();
  293.                 } catch (InterruptedException e) {
  294.                     // Stop waiting and return anyway.
  295.                 } finally {
  296.                     errorThread = null;
  297.                 }
  298.             }

  299.             super.close();
  300.         }
  301.     }

  302.     class SshPushConnection extends BasePackPushConnection {
  303.         private final Process process;

  304.         private StreamCopyThread errorThread;

  305.         SshPushConnection() throws TransportException {
  306.             super(TransportGitSsh.this);
  307.             try {
  308.                 process = getSession().exec(commandFor(getOptionReceivePack()),
  309.                         getTimeout());
  310.                 final MessageWriter msg = new MessageWriter();
  311.                 setMessageWriter(msg);

  312.                 final InputStream rpErr = process.getErrorStream();
  313.                 errorThread = new StreamCopyThread(rpErr, msg.getRawStream());
  314.                 errorThread.start();

  315.                 init(process.getInputStream(), process.getOutputStream());

  316.             } catch (TransportException err) {
  317.                 try {
  318.                     close();
  319.                 } catch (Exception e) {
  320.                     // ignore
  321.                 }
  322.                 throw err;
  323.             } catch (Throwable err) {
  324.                 try {
  325.                     close();
  326.                 } catch (Exception e) {
  327.                     // ignore
  328.                 }
  329.                 throw new TransportException(uri,
  330.                         JGitText.get().remoteHungUpUnexpectedly, err);
  331.             }

  332.             try {
  333.                 readAdvertisedRefs();
  334.             } catch (NoRemoteRepositoryException notFound) {
  335.                 final String msgs = getMessages();
  336.                 checkExecFailure(process.exitValue(), getOptionReceivePack(),
  337.                         msgs);
  338.                 throw cleanNotFound(notFound, msgs);
  339.             }
  340.         }

  341.         @Override
  342.         public void close() {
  343.             endOut();

  344.             if (process != null) {
  345.                 process.destroy();
  346.             }
  347.             if (errorThread != null) {
  348.                 try {
  349.                     errorThread.halt();
  350.                 } catch (InterruptedException e) {
  351.                     // Stop waiting and return anyway.
  352.                 } finally {
  353.                     errorThread = null;
  354.                 }
  355.             }

  356.             super.close();
  357.         }
  358.     }
  359. }