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