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>
6    * and other copyright owners as documented in the project's IP log.
7    *
8    * This program and the accompanying materials are made available
9    * under the terms of the Eclipse Distribution License v1.0 which
10   * accompanies this distribution, is reproduced below, and is
11   * available at http://www.eclipse.org/org/documents/edl-v10.php
12   *
13   * All rights reserved.
14   *
15   * Redistribution and use in source and binary forms, with or
16   * without modification, are permitted provided that the following
17   * conditions are met:
18   *
19   * - Redistributions of source code must retain the above copyright
20   *   notice, this list of conditions and the following disclaimer.
21   *
22   * - Redistributions in binary form must reproduce the above
23   *   copyright notice, this list of conditions and the following
24   *   disclaimer in the documentation and/or other materials provided
25   *   with the distribution.
26   *
27   * - Neither the name of the Eclipse Foundation, Inc. nor the
28   *   names of its contributors may be used to endorse or promote
29   *   products derived from this software without specific prior
30   *   written permission.
31   *
32   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
33   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
34   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
35   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
36   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
37   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
38   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
39   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
40   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
41   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
42   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
43   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
44   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45   */
46  
47  package org.eclipse.jgit.transport;
48  
49  import java.io.File;
50  import java.io.IOException;
51  import java.io.InputStream;
52  import java.text.MessageFormat;
53  import java.util.ArrayList;
54  import java.util.Arrays;
55  import java.util.Collections;
56  import java.util.EnumSet;
57  import java.util.LinkedHashSet;
58  import java.util.List;
59  import java.util.Locale;
60  import java.util.Set;
61  
62  import org.eclipse.jgit.errors.NoRemoteRepositoryException;
63  import org.eclipse.jgit.errors.NotSupportedException;
64  import org.eclipse.jgit.errors.TransportException;
65  import org.eclipse.jgit.internal.JGitText;
66  import org.eclipse.jgit.lib.Constants;
67  import org.eclipse.jgit.lib.Repository;
68  import org.eclipse.jgit.util.FS;
69  import org.eclipse.jgit.util.QuotedString;
70  import org.eclipse.jgit.util.SystemReader;
71  import org.eclipse.jgit.util.io.MessageWriter;
72  import org.eclipse.jgit.util.io.StreamCopyThread;
73  
74  /**
75   * Transport through an SSH tunnel.
76   * <p>
77   * The SSH transport requires the remote side to have Git installed, as the
78   * transport logs into the remote system and executes a Git helper program on
79   * the remote side to read (or write) the remote repository's files.
80   * <p>
81   * This transport does not support direct SCP style of copying files, as it
82   * assumes there are Git specific smarts on the remote side to perform object
83   * enumeration, save file modification and hook execution.
84   */
85  public class TransportGitSsh extends SshTransport implements PackTransport {
86  	static final TransportProtocol PROTO_SSH = new TransportProtocol() {
87  		private final String[] schemeNames = { "ssh", "ssh+git", "git+ssh" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
88  
89  		private final Set<String> schemeSet = Collections
90  				.unmodifiableSet(new LinkedHashSet<>(Arrays
91  						.asList(schemeNames)));
92  
93  		@Override
94  		public String getName() {
95  			return JGitText.get().transportProtoSSH;
96  		}
97  
98  		@Override
99  		public Set<String> getSchemes() {
100 			return schemeSet;
101 		}
102 
103 		@Override
104 		public Set<URIishField> getRequiredFields() {
105 			return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST,
106 					URIishField.PATH));
107 		}
108 
109 		@Override
110 		public Set<URIishField> getOptionalFields() {
111 			return Collections.unmodifiableSet(EnumSet.of(URIishField.USER,
112 					URIishField.PASS, URIishField.PORT));
113 		}
114 
115 		@Override
116 		public int getDefaultPort() {
117 			return 22;
118 		}
119 
120 		@Override
121 		public boolean canHandle(URIish uri, Repository local, String remoteName) {
122 			if (uri.getScheme() == null) {
123 				// scp-style URI "host:path" does not have scheme.
124 				return uri.getHost() != null
125 					&& uri.getPath() != null
126 					&& uri.getHost().length() != 0
127 					&& uri.getPath().length() != 0;
128 			}
129 			return super.canHandle(uri, local, remoteName);
130 		}
131 
132 		@Override
133 		public Transport open(URIish uri, Repository local, String remoteName)
134 				throws NotSupportedException {
135 			return new TransportGitSsh(local, uri);
136 		}
137 
138 		@Override
139 		public Transport open(URIish uri) throws NotSupportedException, TransportException {
140 			return new TransportGitSsh(uri);
141 		}
142 	};
143 
144 	TransportGitSsh(Repository local, URIish uri) {
145 		super(local, uri);
146 		initSshSessionFactory();
147 	}
148 
149 	TransportGitSsh(URIish uri) {
150 		super(uri);
151 		initSshSessionFactory();
152 	}
153 
154 	private void initSshSessionFactory() {
155 		if (useExtSession()) {
156 			setSshSessionFactory(new SshSessionFactory() {
157 				@Override
158 				public RemoteSession getSession(URIish uri2,
159 						CredentialsProvider credentialsProvider, FS fs, int tms)
160 						throws TransportException {
161 					return new ExtSession();
162 				}
163 			});
164 		}
165 	}
166 
167 	/** {@inheritDoc} */
168 	@Override
169 	public FetchConnection openFetch() throws TransportException {
170 		return new SshFetchConnection();
171 	}
172 
173 	/** {@inheritDoc} */
174 	@Override
175 	public PushConnection openPush() throws TransportException {
176 		return new SshPushConnection();
177 	}
178 
179 	String commandFor(String exe) {
180 		String path = uri.getPath();
181 		if (uri.getScheme() != null && uri.getPath().startsWith("/~")) //$NON-NLS-1$
182 			path = (uri.getPath().substring(1));
183 
184 		final StringBuilder cmd = new StringBuilder();
185 		cmd.append(exe);
186 		cmd.append(' ');
187 		cmd.append(QuotedString.BOURNE.quote(path));
188 		return cmd.toString();
189 	}
190 
191 	void checkExecFailure(int status, String exe, String why)
192 			throws TransportException {
193 		if (status == 127) {
194 			IOException cause = null;
195 			if (why != null && why.length() > 0)
196 				cause = new IOException(why);
197 			throw new TransportException(uri, MessageFormat.format(
198 					JGitText.get().cannotExecute, commandFor(exe)), cause);
199 		}
200 	}
201 
202 	NoRemoteRepositoryException cleanNotFound(NoRemoteRepositoryException nf,
203 			String why) {
204 		if (why == null || why.length() == 0)
205 			return nf;
206 
207 		String path = uri.getPath();
208 		if (uri.getScheme() != null && uri.getPath().startsWith("/~")) //$NON-NLS-1$
209 			path = uri.getPath().substring(1);
210 
211 		final StringBuilder pfx = new StringBuilder();
212 		pfx.append("fatal: "); //$NON-NLS-1$
213 		pfx.append(QuotedString.BOURNE.quote(path));
214 		pfx.append(": "); //$NON-NLS-1$
215 		if (why.startsWith(pfx.toString()))
216 			why = why.substring(pfx.length());
217 
218 		return new NoRemoteRepositoryException(uri, why);
219 	}
220 
221 	private static boolean useExtSession() {
222 		return SystemReader.getInstance().getenv("GIT_SSH") != null; //$NON-NLS-1$
223 	}
224 
225 	private class ExtSession implements RemoteSession {
226 		@Override
227 		public Process exec(String command, int timeout)
228 				throws TransportException {
229 			String ssh = SystemReader.getInstance().getenv("GIT_SSH"); //$NON-NLS-1$
230 			boolean putty = ssh.toLowerCase(Locale.ROOT).contains("plink"); //$NON-NLS-1$
231 
232 			List<String> args = new ArrayList<>();
233 			args.add(ssh);
234 			if (putty
235 					&& !ssh.toLowerCase(Locale.ROOT).contains("tortoiseplink")) //$NON-NLS-1$
236 				args.add("-batch"); //$NON-NLS-1$
237 			if (0 < getURI().getPort()) {
238 				args.add(putty ? "-P" : "-p"); //$NON-NLS-1$ //$NON-NLS-2$
239 				args.add(String.valueOf(getURI().getPort()));
240 			}
241 			if (getURI().getUser() != null)
242 				args.add(getURI().getUser() + "@" + getURI().getHost()); //$NON-NLS-1$
243 			else
244 				args.add(getURI().getHost());
245 			args.add(command);
246 
247 			ProcessBuilder pb = createProcess(args);
248 			try {
249 				return pb.start();
250 			} catch (IOException err) {
251 				throw new TransportException(err.getMessage(), err);
252 			}
253 		}
254 
255 		private ProcessBuilder createProcess(List<String> args) {
256 			ProcessBuilder pb = new ProcessBuilder();
257 			pb.command(args);
258 			File directory = local != null ? local.getDirectory() : null;
259 			if (directory != null) {
260 				pb.environment().put(Constants.GIT_DIR_KEY,
261 						directory.getPath());
262 			}
263 			return pb;
264 		}
265 
266 		@Override
267 		public void disconnect() {
268 			// Nothing to do
269 		}
270 	}
271 
272 	class SshFetchConnection extends BasePackFetchConnection {
273 		private final Process process;
274 
275 		private StreamCopyThread errorThread;
276 
277 		SshFetchConnection() throws TransportException {
278 			super(TransportGitSsh.this);
279 			try {
280 				process = getSession().exec(commandFor(getOptionUploadPack()),
281 						getTimeout());
282 				final MessageWriter msg = new MessageWriter();
283 				setMessageWriter(msg);
284 
285 				final InputStream upErr = process.getErrorStream();
286 				errorThread = new StreamCopyThread(upErr, msg.getRawStream());
287 				errorThread.start();
288 
289 				init(process.getInputStream(), process.getOutputStream());
290 
291 			} catch (TransportException err) {
292 				close();
293 				throw err;
294 			} catch (Throwable err) {
295 				close();
296 				throw new TransportException(uri,
297 						JGitText.get().remoteHungUpUnexpectedly, err);
298 			}
299 
300 			try {
301 				readAdvertisedRefs();
302 			} catch (NoRemoteRepositoryException notFound) {
303 				final String msgs = getMessages();
304 				checkExecFailure(process.exitValue(), getOptionUploadPack(),
305 						msgs);
306 				throw cleanNotFound(notFound, msgs);
307 			}
308 		}
309 
310 		@Override
311 		public void close() {
312 			endOut();
313 
314 			if (process != null) {
315 				process.destroy();
316 			}
317 			if (errorThread != null) {
318 				try {
319 					errorThread.halt();
320 				} catch (InterruptedException e) {
321 					// Stop waiting and return anyway.
322 				} finally {
323 					errorThread = null;
324 				}
325 			}
326 
327 			super.close();
328 		}
329 	}
330 
331 	class SshPushConnection extends BasePackPushConnection {
332 		private final Process process;
333 
334 		private StreamCopyThread errorThread;
335 
336 		SshPushConnection() throws TransportException {
337 			super(TransportGitSsh.this);
338 			try {
339 				process = getSession().exec(commandFor(getOptionReceivePack()),
340 						getTimeout());
341 				final MessageWriter msg = new MessageWriter();
342 				setMessageWriter(msg);
343 
344 				final InputStream rpErr = process.getErrorStream();
345 				errorThread = new StreamCopyThread(rpErr, msg.getRawStream());
346 				errorThread.start();
347 
348 				init(process.getInputStream(), process.getOutputStream());
349 
350 			} catch (TransportException err) {
351 				try {
352 					close();
353 				} catch (Exception e) {
354 					// ignore
355 				}
356 				throw err;
357 			} catch (Throwable err) {
358 				try {
359 					close();
360 				} catch (Exception e) {
361 					// ignore
362 				}
363 				throw new TransportException(uri,
364 						JGitText.get().remoteHungUpUnexpectedly, err);
365 			}
366 
367 			try {
368 				readAdvertisedRefs();
369 			} catch (NoRemoteRepositoryException notFound) {
370 				final String msgs = getMessages();
371 				checkExecFailure(process.exitValue(), getOptionReceivePack(),
372 						msgs);
373 				throw cleanNotFound(notFound, msgs);
374 			}
375 		}
376 
377 		@Override
378 		public void close() {
379 			endOut();
380 
381 			if (process != null) {
382 				process.destroy();
383 			}
384 			if (errorThread != null) {
385 				try {
386 					errorThread.halt();
387 				} catch (InterruptedException e) {
388 					// Stop waiting and return anyway.
389 				} finally {
390 					errorThread = null;
391 				}
392 			}
393 
394 			super.close();
395 		}
396 	}
397 }