View Javadoc
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  
14  package org.eclipse.jgit.transport;
15  
16  import java.io.File;
17  import java.io.IOException;
18  import java.io.InputStream;
19  import java.text.MessageFormat;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.EnumSet;
25  import java.util.LinkedHashSet;
26  import java.util.List;
27  import java.util.Locale;
28  import java.util.Map;
29  import java.util.Set;
30  
31  import org.eclipse.jgit.errors.NoRemoteRepositoryException;
32  import org.eclipse.jgit.errors.NotSupportedException;
33  import org.eclipse.jgit.errors.TransportException;
34  import org.eclipse.jgit.internal.JGitText;
35  import org.eclipse.jgit.lib.Constants;
36  import org.eclipse.jgit.lib.Repository;
37  import org.eclipse.jgit.util.FS;
38  import org.eclipse.jgit.util.QuotedString;
39  import org.eclipse.jgit.util.SystemReader;
40  import org.eclipse.jgit.util.io.MessageWriter;
41  import org.eclipse.jgit.util.io.StreamCopyThread;
42  
43  /**
44   * Transport through an SSH tunnel.
45   * <p>
46   * The SSH transport requires the remote side to have Git installed, as the
47   * transport logs into the remote system and executes a Git helper program on
48   * the remote side to read (or write) the remote repository's files.
49   * <p>
50   * This transport does not support direct SCP style of copying files, as it
51   * assumes there are Git specific smarts on the remote side to perform object
52   * enumeration, save file modification and hook execution.
53   */
54  public class TransportGitSsh extends SshTransport implements PackTransport {
55  	private static final String EXT = "ext"; //$NON-NLS-1$
56  
57  	static final TransportProtocol PROTO_SSH = new TransportProtocol() {
58  		private final String[] schemeNames = { "ssh", "ssh+git", "git+ssh" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
59  
60  		private final Set<String> schemeSet = Collections
61  				.unmodifiableSet(new LinkedHashSet<>(Arrays
62  						.asList(schemeNames)));
63  
64  		@Override
65  		public String getName() {
66  			return JGitText.get().transportProtoSSH;
67  		}
68  
69  		@Override
70  		public Set<String> getSchemes() {
71  			return schemeSet;
72  		}
73  
74  		@Override
75  		public Set<URIishField> getRequiredFields() {
76  			return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST,
77  					URIishField.PATH));
78  		}
79  
80  		@Override
81  		public Set<URIishField> getOptionalFields() {
82  			return Collections.unmodifiableSet(EnumSet.of(URIishField.USER,
83  					URIishField.PASS, URIishField.PORT));
84  		}
85  
86  		@Override
87  		public int getDefaultPort() {
88  			return 22;
89  		}
90  
91  		@Override
92  		public boolean canHandle(URIish uri, Repository local, String remoteName) {
93  			if (uri.getScheme() == null) {
94  				// scp-style URI "host:path" does not have scheme.
95  				return uri.getHost() != null
96  					&& uri.getPath() != null
97  					&& uri.getHost().length() != 0
98  					&& uri.getPath().length() != 0;
99  			}
100 			return super.canHandle(uri, local, remoteName);
101 		}
102 
103 		@Override
104 		public Transport open(URIish uri, Repository local, String remoteName)
105 				throws NotSupportedException {
106 			return new TransportGitSsh(local, uri);
107 		}
108 
109 		@Override
110 		public Transport open(URIish uri) throws NotSupportedException, TransportException {
111 			return new TransportGitSsh(uri);
112 		}
113 	};
114 
115 	TransportGitSsh(Repository local, URIish uri) {
116 		super(local, uri);
117 		initSshSessionFactory();
118 	}
119 
120 	TransportGitSsh(URIish uri) {
121 		super(uri);
122 		initSshSessionFactory();
123 	}
124 
125 	private void initSshSessionFactory() {
126 		if (useExtSession()) {
127 			setSshSessionFactory(new SshSessionFactory() {
128 				@Override
129 				public RemoteSession getSession(URIish uri2,
130 						CredentialsProvider credentialsProvider, FS fs, int tms)
131 						throws TransportException {
132 					return new ExtSession();
133 				}
134 
135 				@Override
136 				public String getType() {
137 					return EXT;
138 				}
139 			});
140 		}
141 	}
142 
143 	/** {@inheritDoc} */
144 	@Override
145 	public FetchConnection openFetch() throws TransportException {
146 		return new SshFetchConnection();
147 	}
148 
149 	@Override
150 	public FetchConnection openFetch(Collection<RefSpec> refSpecs,
151 			String... additionalPatterns)
152 			throws NotSupportedException, TransportException {
153 		return new SshFetchConnection(refSpecs, additionalPatterns);
154 	}
155 
156 	/** {@inheritDoc} */
157 	@Override
158 	public PushConnection openPush() throws TransportException {
159 		return new SshPushConnection();
160 	}
161 
162 	String commandFor(String exe) {
163 		String path = uri.getPath();
164 		if (uri.getScheme() != null && uri.getPath().startsWith("/~")) //$NON-NLS-1$
165 			path = (uri.getPath().substring(1));
166 
167 		final StringBuilder cmd = new StringBuilder();
168 		cmd.append(exe);
169 		cmd.append(' ');
170 		cmd.append(QuotedString.BOURNE.quote(path));
171 		return cmd.toString();
172 	}
173 
174 	void checkExecFailure(int status, String exe, String why)
175 			throws TransportException {
176 		if (status == 127) {
177 			IOException cause = null;
178 			if (why != null && why.length() > 0)
179 				cause = new IOException(why);
180 			throw new TransportException(uri, MessageFormat.format(
181 					JGitText.get().cannotExecute, commandFor(exe)), cause);
182 		}
183 	}
184 
185 	NoRemoteRepositoryException cleanNotFound(NoRemoteRepositoryException nf,
186 			String why) {
187 		if (why == null || why.length() == 0)
188 			return nf;
189 
190 		String path = uri.getPath();
191 		if (uri.getScheme() != null && uri.getPath().startsWith("/~")) //$NON-NLS-1$
192 			path = uri.getPath().substring(1);
193 
194 		final StringBuilder pfx = new StringBuilder();
195 		pfx.append("fatal: "); //$NON-NLS-1$
196 		pfx.append(QuotedString.BOURNE.quote(path));
197 		pfx.append(": "); //$NON-NLS-1$
198 		if (why.startsWith(pfx.toString()))
199 			why = why.substring(pfx.length());
200 
201 		return new NoRemoteRepositoryException(uri, why);
202 	}
203 
204 	private static boolean useExtSession() {
205 		return SystemReader.getInstance().getenv("GIT_SSH") != null; //$NON-NLS-1$
206 	}
207 
208 	private class ExtSession implements RemoteSession2 {
209 
210 		@Override
211 		public Process exec(String command, int timeout)
212 				throws TransportException {
213 			return exec(command, null, timeout);
214 		}
215 
216 		@Override
217 		public Process exec(String command, Map<String, String> environment,
218 				int timeout) throws TransportException {
219 			String ssh = SystemReader.getInstance().getenv("GIT_SSH"); //$NON-NLS-1$
220 			boolean putty = ssh.toLowerCase(Locale.ROOT).contains("plink"); //$NON-NLS-1$
221 
222 			List<String> args = new ArrayList<>();
223 			args.add(ssh);
224 			if (putty && !ssh.toLowerCase(Locale.ROOT)
225 					.contains("tortoiseplink")) {//$NON-NLS-1$
226 				args.add("-batch"); //$NON-NLS-1$
227 			}
228 			if (0 < getURI().getPort()) {
229 				args.add(putty ? "-P" : "-p"); //$NON-NLS-1$ //$NON-NLS-2$
230 				args.add(String.valueOf(getURI().getPort()));
231 			}
232 			if (getURI().getUser() != null) {
233 				args.add(getURI().getUser() + "@" + getURI().getHost()); //$NON-NLS-1$
234 			} else {
235 				args.add(getURI().getHost());
236 			}
237 			args.add(command);
238 
239 			ProcessBuilder pb = createProcess(args, environment);
240 			try {
241 				return pb.start();
242 			} catch (IOException err) {
243 				throw new TransportException(err.getMessage(), err);
244 			}
245 		}
246 
247 		private ProcessBuilder createProcess(List<String> args,
248 				Map<String, String> environment) {
249 			ProcessBuilder pb = new ProcessBuilder();
250 			pb.command(args);
251 			if (environment != null) {
252 				pb.environment().putAll(environment);
253 			}
254 			File directory = local != null ? local.getDirectory() : null;
255 			if (directory != null) {
256 				pb.environment().put(Constants.GIT_DIR_KEY,
257 						directory.getPath());
258 			}
259 			return pb;
260 		}
261 
262 		@Override
263 		public void disconnect() {
264 			// Nothing to do
265 		}
266 	}
267 
268 	class SshFetchConnection extends BasePackFetchConnection {
269 		private final Process process;
270 
271 		private StreamCopyThread errorThread;
272 
273 		SshFetchConnection() throws TransportException {
274 			this(Collections.emptyList());
275 		}
276 
277 		SshFetchConnection(Collection<RefSpec> refSpecs,
278 				String... additionalPatterns) throws TransportException {
279 			super(TransportGitSsh.this);
280 			try {
281 				RemoteSession session = getSession();
282 				TransferConfig.ProtocolVersion gitProtocol = protocol;
283 				if (gitProtocol == null) {
284 					gitProtocol = TransferConfig.ProtocolVersion.V2;
285 				}
286 				if (session instanceof RemoteSession2
287 						&& TransferConfig.ProtocolVersion.V2
288 								.equals(gitProtocol)) {
289 					process = ((RemoteSession2) session).exec(
290 							commandFor(getOptionUploadPack()), Collections
291 									.singletonMap(
292 											GitProtocolConstants.PROTOCOL_ENVIRONMENT_VARIABLE,
293 											GitProtocolConstants.VERSION_2_REQUEST),
294 							getTimeout());
295 				} else {
296 					process = session.exec(commandFor(getOptionUploadPack()),
297 							getTimeout());
298 				}
299 				final MessageWriter msg = new MessageWriter();
300 				setMessageWriter(msg);
301 
302 				final InputStream upErr = process.getErrorStream();
303 				errorThread = new StreamCopyThread(upErr, msg.getRawStream());
304 				errorThread.start();
305 
306 				init(process.getInputStream(), process.getOutputStream());
307 
308 			} catch (TransportException err) {
309 				close();
310 				throw err;
311 			} catch (Throwable err) {
312 				close();
313 				throw new TransportException(uri,
314 						JGitText.get().remoteHungUpUnexpectedly, err);
315 			}
316 
317 			try {
318 				if (!readAdvertisedRefs()) {
319 					lsRefs(refSpecs, additionalPatterns);
320 				}
321 			} catch (NoRemoteRepositoryException notFound) {
322 				final String msgs = getMessages();
323 				checkExecFailure(process.exitValue(), getOptionUploadPack(),
324 						msgs);
325 				throw cleanNotFound(notFound, msgs);
326 			}
327 		}
328 
329 		@Override
330 		public void close() {
331 			endOut();
332 
333 			if (process != null) {
334 				process.destroy();
335 			}
336 			if (errorThread != null) {
337 				try {
338 					errorThread.halt();
339 				} catch (InterruptedException e) {
340 					// Stop waiting and return anyway.
341 				} finally {
342 					errorThread = null;
343 				}
344 			}
345 
346 			super.close();
347 		}
348 	}
349 
350 	class SshPushConnection extends BasePackPushConnection {
351 		private final Process process;
352 
353 		private StreamCopyThread errorThread;
354 
355 		SshPushConnection() throws TransportException {
356 			super(TransportGitSsh.this);
357 			try {
358 				process = getSession().exec(commandFor(getOptionReceivePack()),
359 						getTimeout());
360 				final MessageWriter msg = new MessageWriter();
361 				setMessageWriter(msg);
362 
363 				final InputStream rpErr = process.getErrorStream();
364 				errorThread = new StreamCopyThread(rpErr, msg.getRawStream());
365 				errorThread.start();
366 
367 				init(process.getInputStream(), process.getOutputStream());
368 
369 			} catch (TransportException err) {
370 				try {
371 					close();
372 				} catch (Exception e) {
373 					// ignore
374 				}
375 				throw err;
376 			} catch (Throwable err) {
377 				try {
378 					close();
379 				} catch (Exception e) {
380 					// ignore
381 				}
382 				throw new TransportException(uri,
383 						JGitText.get().remoteHungUpUnexpectedly, err);
384 			}
385 
386 			try {
387 				readAdvertisedRefs();
388 			} catch (NoRemoteRepositoryException notFound) {
389 				final String msgs = getMessages();
390 				checkExecFailure(process.exitValue(), getOptionReceivePack(),
391 						msgs);
392 				throw cleanNotFound(notFound, msgs);
393 			}
394 		}
395 
396 		@Override
397 		public void close() {
398 			endOut();
399 
400 			if (process != null) {
401 				process.destroy();
402 			}
403 			if (errorThread != null) {
404 				try {
405 					errorThread.halt();
406 				} catch (InterruptedException e) {
407 					// Stop waiting and return anyway.
408 				} finally {
409 					errorThread = null;
410 				}
411 			}
412 
413 			super.close();
414 		}
415 	}
416 }