View Javadoc
1   /*
2    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.transport;
45  
46  import java.io.BufferedReader;
47  import java.io.FileNotFoundException;
48  import java.io.IOException;
49  import java.io.OutputStream;
50  import java.text.MessageFormat;
51  import java.util.ArrayList;
52  import java.util.Collection;
53  import java.util.Collections;
54  import java.util.Comparator;
55  import java.util.EnumSet;
56  import java.util.HashMap;
57  import java.util.List;
58  import java.util.Map;
59  import java.util.Set;
60  import java.util.TreeMap;
61  
62  import org.eclipse.jgit.errors.NotSupportedException;
63  import org.eclipse.jgit.errors.TransportException;
64  import org.eclipse.jgit.internal.JGitText;
65  import org.eclipse.jgit.lib.Constants;
66  import org.eclipse.jgit.lib.ObjectId;
67  import org.eclipse.jgit.lib.ObjectIdRef;
68  import org.eclipse.jgit.lib.ProgressMonitor;
69  import org.eclipse.jgit.lib.Ref;
70  import org.eclipse.jgit.lib.Ref.Storage;
71  import org.eclipse.jgit.lib.Repository;
72  import org.eclipse.jgit.lib.SymbolicRef;
73  
74  import com.jcraft.jsch.Channel;
75  import com.jcraft.jsch.ChannelSftp;
76  import com.jcraft.jsch.JSchException;
77  import com.jcraft.jsch.SftpATTRS;
78  import com.jcraft.jsch.SftpException;
79  
80  /**
81   * Transport over the non-Git aware SFTP (SSH based FTP) protocol.
82   * <p>
83   * The SFTP transport does not require any specialized Git support on the remote
84   * (server side) repository. Object files are retrieved directly through secure
85   * shell's FTP protocol, making it possible to copy objects from a remote
86   * repository that is available over SSH, but whose remote host does not have
87   * Git installed.
88   * <p>
89   * Unlike the HTTP variant (see
90   * {@link org.eclipse.jgit.transport.TransportHttp}) we rely upon being able to
91   * list files in directories, as the SFTP protocol supports this function. By
92   * listing files through SFTP we can avoid needing to have current
93   * <code>objects/info/packs</code> or <code>info/refs</code> files on the remote
94   * repository and access the data directly, much as Git itself would.
95   * <p>
96   * Concurrent pushing over this transport is not supported. Multiple concurrent
97   * push operations may cause confusion in the repository state.
98   *
99   * @see WalkFetchConnection
100  */
101 public class TransportSftp extends SshTransport implements WalkTransport {
102 	static final TransportProtocol PROTO_SFTP = new TransportProtocol() {
103 		@Override
104 		public String getName() {
105 			return JGitText.get().transportProtoSFTP;
106 		}
107 
108 		@Override
109 		public Set<String> getSchemes() {
110 			return Collections.singleton("sftp"); //$NON-NLS-1$
111 		}
112 
113 		@Override
114 		public Set<URIishField> getRequiredFields() {
115 			return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST,
116 					URIishField.PATH));
117 		}
118 
119 		@Override
120 		public Set<URIishField> getOptionalFields() {
121 			return Collections.unmodifiableSet(EnumSet.of(URIishField.USER,
122 					URIishField.PASS, URIishField.PORT));
123 		}
124 
125 		@Override
126 		public int getDefaultPort() {
127 			return 22;
128 		}
129 
130 		@Override
131 		public Transport open(URIish uri, Repository local, String remoteName)
132 				throws NotSupportedException {
133 			return new TransportSftp(local, uri);
134 		}
135 	};
136 
137 	TransportSftp(final Repository local, final URIish uri) {
138 		super(local, uri);
139 	}
140 
141 	/** {@inheritDoc} */
142 	@Override
143 	public FetchConnection openFetch() throws TransportException {
144 		final SftpObjectDB c = new SftpObjectDB(uri.getPath());
145 		final WalkFetchConnection r = new WalkFetchConnection(this, c);
146 		r.available(c.readAdvertisedRefs());
147 		return r;
148 	}
149 
150 	/** {@inheritDoc} */
151 	@Override
152 	public PushConnection openPush() throws TransportException {
153 		final SftpObjectDB c = new SftpObjectDB(uri.getPath());
154 		final WalkPushConnection r = new WalkPushConnection(this, c);
155 		r.available(c.readAdvertisedRefs());
156 		return r;
157 	}
158 
159 	ChannelSftp newSftp() throws TransportException {
160 		final int tms = getTimeout() > 0 ? getTimeout() * 1000 : 0;
161 		try {
162 			// @TODO: Fix so that this operation is generic and casting to
163 			// JschSession is no longer necessary.
164 			final Channel channel = ((JschSession) getSession())
165 					.getSftpChannel();
166 			channel.connect(tms);
167 			return (ChannelSftp) channel;
168 		} catch (JSchException je) {
169 			throw new TransportException(uri, je.getMessage(), je);
170 		}
171 	}
172 
173 	class SftpObjectDB extends WalkRemoteObjectDatabase {
174 		private final String objectsPath;
175 
176 		private ChannelSftp ftp;
177 
178 		SftpObjectDB(String path) throws TransportException {
179 			if (path.startsWith("/~")) //$NON-NLS-1$
180 				path = path.substring(1);
181 			if (path.startsWith("~/")) //$NON-NLS-1$
182 				path = path.substring(2);
183 			try {
184 				ftp = newSftp();
185 				ftp.cd(path);
186 				ftp.cd("objects"); //$NON-NLS-1$
187 				objectsPath = ftp.pwd();
188 			} catch (TransportException err) {
189 				close();
190 				throw err;
191 			} catch (SftpException je) {
192 				throw new TransportException(MessageFormat.format(
193 						JGitText.get().cannotEnterObjectsPath, path,
194 						je.getMessage()), je);
195 			}
196 		}
197 
198 		SftpObjectDB(final SftpObjectDB parent, final String p)
199 				throws TransportException {
200 			try {
201 				ftp = newSftp();
202 				ftp.cd(parent.objectsPath);
203 				ftp.cd(p);
204 				objectsPath = ftp.pwd();
205 			} catch (TransportException err) {
206 				close();
207 				throw err;
208 			} catch (SftpException je) {
209 				throw new TransportException(MessageFormat.format(
210 						JGitText.get().cannotEnterPathFromParent, p,
211 						parent.objectsPath, je.getMessage()), je);
212 			}
213 		}
214 
215 		@Override
216 		URIish getURI() {
217 			return uri.setPath(objectsPath);
218 		}
219 
220 		@Override
221 		Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException {
222 			try {
223 				return readAlternates(INFO_ALTERNATES);
224 			} catch (FileNotFoundException err) {
225 				return null;
226 			}
227 		}
228 
229 		@Override
230 		WalkRemoteObjectDatabase openAlternate(final String location)
231 				throws IOException {
232 			return new SftpObjectDB(this, location);
233 		}
234 
235 		@Override
236 		Collection<String> getPackNames() throws IOException {
237 			final List<String> packs = new ArrayList<>();
238 			try {
239 				@SuppressWarnings("unchecked")
240 				final Collection<ChannelSftp.LsEntry> list = ftp.ls("pack"); //$NON-NLS-1$
241 				final HashMap<String, ChannelSftp.LsEntry> files;
242 				final HashMap<String, Integer> mtimes;
243 
244 				files = new HashMap<>();
245 				mtimes = new HashMap<>();
246 
247 				for (final ChannelSftp.LsEntry ent : list)
248 					files.put(ent.getFilename(), ent);
249 				for (final ChannelSftp.LsEntry ent : list) {
250 					final String n = ent.getFilename();
251 					if (!n.startsWith("pack-") || !n.endsWith(".pack")) //$NON-NLS-1$ //$NON-NLS-2$
252 						continue;
253 
254 					final String in = n.substring(0, n.length() - 5) + ".idx"; //$NON-NLS-1$
255 					if (!files.containsKey(in))
256 						continue;
257 
258 					mtimes.put(n, Integer.valueOf(ent.getAttrs().getMTime()));
259 					packs.add(n);
260 				}
261 
262 				Collections.sort(packs, new Comparator<String>() {
263 					@Override
264 					public int compare(final String o1, final String o2) {
265 						return mtimes.get(o2).intValue()
266 								- mtimes.get(o1).intValue();
267 					}
268 				});
269 			} catch (SftpException je) {
270 				throw new TransportException(
271 						MessageFormat.format(JGitText.get().cannotListPackPath,
272 								objectsPath, je.getMessage()),
273 						je);
274 			}
275 			return packs;
276 		}
277 
278 		@Override
279 		FileStream open(final String path) throws IOException {
280 			try {
281 				final SftpATTRS a = ftp.lstat(path);
282 				return new FileStream(ftp.get(path), a.getSize());
283 			} catch (SftpException je) {
284 				if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
285 					throw new FileNotFoundException(path);
286 				throw new TransportException(MessageFormat.format(
287 						JGitText.get().cannotGetObjectsPath, objectsPath, path,
288 						je.getMessage()), je);
289 			}
290 		}
291 
292 		@Override
293 		void deleteFile(final String path) throws IOException {
294 			try {
295 				ftp.rm(path);
296 			} catch (SftpException je) {
297 				if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
298 					return;
299 				throw new TransportException(MessageFormat.format(
300 						JGitText.get().cannotDeleteObjectsPath, objectsPath,
301 						path, je.getMessage()), je);
302 			}
303 
304 			// Prune any now empty directories.
305 			//
306 			String dir = path;
307 			int s = dir.lastIndexOf('/');
308 			while (s > 0) {
309 				try {
310 					dir = dir.substring(0, s);
311 					ftp.rmdir(dir);
312 					s = dir.lastIndexOf('/');
313 				} catch (SftpException je) {
314 					// If we cannot delete it, leave it alone. It may have
315 					// entries still in it, or maybe we lack write access on
316 					// the parent. Either way it isn't a fatal error.
317 					//
318 					break;
319 				}
320 			}
321 		}
322 
323 		@Override
324 		OutputStream writeFile(final String path,
325 				final ProgressMonitor monitor, final String monitorTask)
326 				throws IOException {
327 			try {
328 				return ftp.put(path);
329 			} catch (SftpException je) {
330 				if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
331 					mkdir_p(path);
332 					try {
333 						return ftp.put(path);
334 					} catch (SftpException je2) {
335 						je = je2;
336 					}
337 				}
338 
339 				throw new TransportException(MessageFormat.format(
340 						JGitText.get().cannotWriteObjectsPath, objectsPath,
341 						path, je.getMessage()), je);
342 			}
343 		}
344 
345 		@Override
346 		void writeFile(final String path, final byte[] data) throws IOException {
347 			final String lock = path + ".lock"; //$NON-NLS-1$
348 			try {
349 				super.writeFile(lock, data);
350 				try {
351 					ftp.rename(lock, path);
352 				} catch (SftpException je) {
353 					throw new TransportException(MessageFormat.format(
354 							JGitText.get().cannotWriteObjectsPath, objectsPath,
355 							path, je.getMessage()), je);
356 				}
357 			} catch (IOException err) {
358 				try {
359 					ftp.rm(lock);
360 				} catch (SftpException e) {
361 					// Ignore deletion failure, we are already
362 					// failing anyway.
363 				}
364 				throw err;
365 			}
366 		}
367 
368 		private void mkdir_p(String path) throws IOException {
369 			final int s = path.lastIndexOf('/');
370 			if (s <= 0)
371 				return;
372 
373 			path = path.substring(0, s);
374 			try {
375 				ftp.mkdir(path);
376 			} catch (SftpException je) {
377 				if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
378 					mkdir_p(path);
379 					try {
380 						ftp.mkdir(path);
381 						return;
382 					} catch (SftpException je2) {
383 						je = je2;
384 					}
385 				}
386 
387 				throw new TransportException(MessageFormat.format(
388 						JGitText.get().cannotMkdirObjectPath, objectsPath, path,
389 						je.getMessage()), je);
390 			}
391 		}
392 
393 		Map<String, Ref> readAdvertisedRefs() throws TransportException {
394 			final TreeMap<String, Ref> avail = new TreeMap<>();
395 			readPackedRefs(avail);
396 			readRef(avail, ROOT_DIR + Constants.HEAD, Constants.HEAD);
397 			readLooseRefs(avail, ROOT_DIR + "refs", "refs/"); //$NON-NLS-1$ //$NON-NLS-2$
398 			return avail;
399 		}
400 
401 		@SuppressWarnings("unchecked")
402 		private void readLooseRefs(final TreeMap<String, Ref> avail,
403 				final String dir, final String prefix)
404 				throws TransportException {
405 			final Collection<ChannelSftp.LsEntry> list;
406 			try {
407 				list = ftp.ls(dir);
408 			} catch (SftpException je) {
409 				throw new TransportException(MessageFormat.format(
410 						JGitText.get().cannotListObjectsPath, objectsPath, dir,
411 						je.getMessage()), je);
412 			}
413 
414 			for (final ChannelSftp.LsEntry ent : list) {
415 				final String n = ent.getFilename();
416 				if (".".equals(n) || "..".equals(n)) //$NON-NLS-1$ //$NON-NLS-2$
417 					continue;
418 
419 				final String nPath = dir + "/" + n; //$NON-NLS-1$
420 				if (ent.getAttrs().isDir())
421 					readLooseRefs(avail, nPath, prefix + n + "/"); //$NON-NLS-1$
422 				else
423 					readRef(avail, nPath, prefix + n);
424 			}
425 		}
426 
427 		private Ref readRef(final TreeMap<String, Ref> avail,
428 				final String path, final String name) throws TransportException {
429 			final String line;
430 			try {
431 				final BufferedReader br = openReader(path);
432 				try {
433 					line = br.readLine();
434 				} finally {
435 					br.close();
436 				}
437 			} catch (FileNotFoundException noRef) {
438 				return null;
439 			} catch (IOException err) {
440 				throw new TransportException(MessageFormat.format(
441 						JGitText.get().cannotReadObjectsPath, objectsPath, path,
442 						err.getMessage()), err);
443 			}
444 
445 			if (line == null)
446 				throw new TransportException(
447 						MessageFormat.format(JGitText.get().emptyRef, name));
448 
449 			if (line.startsWith("ref: ")) { //$NON-NLS-1$
450 				final String target = line.substring("ref: ".length()); //$NON-NLS-1$
451 				Ref r = avail.get(target);
452 				if (r == null)
453 					r = readRef(avail, ROOT_DIR + target, target);
454 				if (r == null)
455 					r = new ObjectIdRef.Unpeeled(Ref.Storage.NEW, target, null);
456 				r = new SymbolicRef(name, r);
457 				avail.put(r.getName(), r);
458 				return r;
459 			}
460 
461 			if (ObjectId.isId(line)) {
462 				final Ref r = new ObjectIdRef.Unpeeled(loose(avail.get(name)),
463 						name, ObjectId.fromString(line));
464 				avail.put(r.getName(), r);
465 				return r;
466 			}
467 
468 			throw new TransportException(
469 					MessageFormat.format(JGitText.get().badRef, name, line));
470 		}
471 
472 		private Storage loose(final Ref r) {
473 			if (r != null && r.getStorage() == Storage.PACKED)
474 				return Storage.LOOSE_PACKED;
475 			return Storage.LOOSE;
476 		}
477 
478 		@Override
479 		void close() {
480 			if (ftp != null) {
481 				try {
482 					if (ftp.isConnected())
483 						ftp.disconnect();
484 				} finally {
485 					ftp = null;
486 				}
487 			}
488 		}
489 	}
490 }