You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

TransportGitSsh.java 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  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. }