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