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(final Repository local, final URIish uri) {
145 		super(local, uri);
146 		initSshSessionFactory();
147 	}
148 
149 	TransportGitSsh(final 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 	@Override
168 	public FetchConnection openFetch() throws TransportException {
169 		return new SshFetchConnection();
170 	}
171 
172 	@Override
173 	public PushConnection openPush() throws TransportException {
174 		return new SshPushConnection();
175 	}
176 
177 	String commandFor(final String exe) {
178 		String path = uri.getPath();
179 		if (uri.getScheme() != null && uri.getPath().startsWith("/~")) //$NON-NLS-1$
180 			path = (uri.getPath().substring(1));
181 
182 		final StringBuilder cmd = new StringBuilder();
183 		cmd.append(exe);
184 		cmd.append(' ');
185 		cmd.append(QuotedString.BOURNE.quote(path));
186 		return cmd.toString();
187 	}
188 
189 	void checkExecFailure(int status, String exe, String why)
190 			throws TransportException {
191 		if (status == 127) {
192 			IOException cause = null;
193 			if (why != null && why.length() > 0)
194 				cause = new IOException(why);
195 			throw new TransportException(uri, MessageFormat.format(
196 					JGitText.get().cannotExecute, commandFor(exe)), cause);
197 		}
198 	}
199 
200 	NoRemoteRepositoryException cleanNotFound(NoRemoteRepositoryException nf,
201 			String why) {
202 		if (why == null || why.length() == 0)
203 			return nf;
204 
205 		String path = uri.getPath();
206 		if (uri.getScheme() != null && uri.getPath().startsWith("/~")) //$NON-NLS-1$
207 			path = uri.getPath().substring(1);
208 
209 		final StringBuilder pfx = new StringBuilder();
210 		pfx.append("fatal: "); //$NON-NLS-1$
211 		pfx.append(QuotedString.BOURNE.quote(path));
212 		pfx.append(": "); //$NON-NLS-1$
213 		if (why.startsWith(pfx.toString()))
214 			why = why.substring(pfx.length());
215 
216 		return new NoRemoteRepositoryException(uri, why);
217 	}
218 
219 	private static boolean useExtSession() {
220 		return SystemReader.getInstance().getenv("GIT_SSH") != null; //$NON-NLS-1$
221 	}
222 
223 	private class ExtSession implements RemoteSession {
224 		@Override
225 		public Process exec(String command, int timeout)
226 				throws TransportException {
227 			String ssh = SystemReader.getInstance().getenv("GIT_SSH"); //$NON-NLS-1$
228 			boolean putty = ssh.toLowerCase(Locale.ROOT).contains("plink"); //$NON-NLS-1$
229 
230 			List<String> args = new ArrayList<>();
231 			args.add(ssh);
232 			if (putty
233 					&& !ssh.toLowerCase(Locale.ROOT).contains("tortoiseplink")) //$NON-NLS-1$
234 				args.add("-batch"); //$NON-NLS-1$
235 			if (0 < getURI().getPort()) {
236 				args.add(putty ? "-P" : "-p"); //$NON-NLS-1$ //$NON-NLS-2$
237 				args.add(String.valueOf(getURI().getPort()));
238 			}
239 			if (getURI().getUser() != null)
240 				args.add(getURI().getUser() + "@" + getURI().getHost()); //$NON-NLS-1$
241 			else
242 				args.add(getURI().getHost());
243 			args.add(command);
244 
245 			ProcessBuilder pb = new ProcessBuilder();
246 			pb.command(args);
247 
248 			File directory = local.getDirectory();
249 			if (directory != null)
250 				pb.environment().put(Constants.GIT_DIR_KEY,
251 						directory.getPath());
252 
253 			try {
254 				return pb.start();
255 			} catch (IOException err) {
256 				throw new TransportException(err.getMessage(), err);
257 			}
258 		}
259 
260 		@Override
261 		public void disconnect() {
262 			// Nothing to do
263 		}
264 	}
265 
266 	class SshFetchConnection extends BasePackFetchConnection {
267 		private final Process process;
268 
269 		private StreamCopyThread errorThread;
270 
271 		SshFetchConnection() throws TransportException {
272 			super(TransportGitSsh.this);
273 			try {
274 				process = getSession().exec(commandFor(getOptionUploadPack()),
275 						getTimeout());
276 				final MessageWriter msg = new MessageWriter();
277 				setMessageWriter(msg);
278 
279 				final InputStream upErr = process.getErrorStream();
280 				errorThread = new StreamCopyThread(upErr, msg.getRawStream());
281 				errorThread.start();
282 
283 				init(process.getInputStream(), process.getOutputStream());
284 
285 			} catch (TransportException err) {
286 				close();
287 				throw err;
288 			} catch (IOException err) {
289 				close();
290 				throw new TransportException(uri,
291 						JGitText.get().remoteHungUpUnexpectedly, err);
292 			}
293 
294 			try {
295 				readAdvertisedRefs();
296 			} catch (NoRemoteRepositoryException notFound) {
297 				final String msgs = getMessages();
298 				checkExecFailure(process.exitValue(), getOptionUploadPack(),
299 						msgs);
300 				throw cleanNotFound(notFound, msgs);
301 			}
302 		}
303 
304 		@Override
305 		public void close() {
306 			endOut();
307 
308 			if (errorThread != null) {
309 				try {
310 					errorThread.halt();
311 				} catch (InterruptedException e) {
312 					// Stop waiting and return anyway.
313 				} finally {
314 					errorThread = null;
315 				}
316 			}
317 
318 			super.close();
319 			if (process != null)
320 				process.destroy();
321 		}
322 	}
323 
324 	class SshPushConnection extends BasePackPushConnection {
325 		private final Process process;
326 
327 		private StreamCopyThread errorThread;
328 
329 		SshPushConnection() throws TransportException {
330 			super(TransportGitSsh.this);
331 			try {
332 				process = getSession().exec(commandFor(getOptionReceivePack()),
333 						getTimeout());
334 				final MessageWriter msg = new MessageWriter();
335 				setMessageWriter(msg);
336 
337 				final InputStream rpErr = process.getErrorStream();
338 				errorThread = new StreamCopyThread(rpErr, msg.getRawStream());
339 				errorThread.start();
340 
341 				init(process.getInputStream(), process.getOutputStream());
342 
343 			} catch (TransportException err) {
344 				close();
345 				throw err;
346 			} catch (IOException err) {
347 				close();
348 				throw new TransportException(uri,
349 						JGitText.get().remoteHungUpUnexpectedly, err);
350 			}
351 
352 			try {
353 				readAdvertisedRefs();
354 			} catch (NoRemoteRepositoryException notFound) {
355 				final String msgs = getMessages();
356 				checkExecFailure(process.exitValue(), getOptionReceivePack(),
357 						msgs);
358 				throw cleanNotFound(notFound, msgs);
359 			}
360 		}
361 
362 		@Override
363 		public void close() {
364 			endOut();
365 
366 			if (errorThread != null) {
367 				try {
368 					errorThread.halt();
369 				} catch (InterruptedException e) {
370 					// Stop waiting and return anyway.
371 				} finally {
372 					errorThread = null;
373 				}
374 			}
375 
376 			super.close();
377 			if (process != null)
378 				process.destroy();
379 		}
380 	}
381 }