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 static org.eclipse.jgit.lib.Constants.LOCK_SUFFIX;
47  
48  import java.io.BufferedReader;
49  import java.io.FileNotFoundException;
50  import java.io.IOException;
51  import java.io.OutputStream;
52  import java.text.MessageFormat;
53  import java.util.ArrayList;
54  import java.util.Collection;
55  import java.util.Collections;
56  import java.util.Comparator;
57  import java.util.EnumSet;
58  import java.util.HashMap;
59  import java.util.List;
60  import java.util.Map;
61  import java.util.Set;
62  import java.util.TreeMap;
63  
64  import org.eclipse.jgit.errors.NotSupportedException;
65  import org.eclipse.jgit.errors.TransportException;
66  import org.eclipse.jgit.internal.JGitText;
67  import org.eclipse.jgit.lib.Constants;
68  import org.eclipse.jgit.lib.ObjectId;
69  import org.eclipse.jgit.lib.ObjectIdRef;
70  import org.eclipse.jgit.lib.ProgressMonitor;
71  import org.eclipse.jgit.lib.Ref;
72  import org.eclipse.jgit.lib.Ref.Storage;
73  import org.eclipse.jgit.lib.Repository;
74  import org.eclipse.jgit.lib.SymbolicRef;
75  
76  import com.jcraft.jsch.Channel;
77  import com.jcraft.jsch.ChannelSftp;
78  import com.jcraft.jsch.JSchException;
79  import com.jcraft.jsch.SftpATTRS;
80  import com.jcraft.jsch.SftpException;
81  
82  /**
83   * Transport over the non-Git aware SFTP (SSH based FTP) protocol.
84   * <p>
85   * The SFTP transport does not require any specialized Git support on the remote
86   * (server side) repository. Object files are retrieved directly through secure
87   * shell's FTP protocol, making it possible to copy objects from a remote
88   * repository that is available over SSH, but whose remote host does not have
89   * Git installed.
90   * <p>
91   * Unlike the HTTP variant (see {@link TransportHttp}) we rely upon being able
92   * to list files in directories, as the SFTP protocol supports this function. By
93   * listing files through SFTP we can avoid needing to have current
94   * <code>objects/info/packs</code> or <code>info/refs</code> files on the
95   * remote repository and access the data directly, much as Git itself would.
96   * <p>
97   * Concurrent pushing over this transport is not supported. Multiple concurrent
98   * push operations may cause confusion in the repository state.
99   *
100  * @see WalkFetchConnection
101  */
102 public class TransportSftp extends SshTransport implements WalkTransport {
103 	static final TransportProtocol PROTO_SFTP = new TransportProtocol() {
104 		@Override
105 		public String getName() {
106 			return JGitText.get().transportProtoSFTP;
107 		}
108 
109 		@Override
110 		public Set<String> getSchemes() {
111 			return Collections.singleton("sftp"); //$NON-NLS-1$
112 		}
113 
114 		@Override
115 		public Set<URIishField> getRequiredFields() {
116 			return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST,
117 					URIishField.PATH));
118 		}
119 
120 		@Override
121 		public Set<URIishField> getOptionalFields() {
122 			return Collections.unmodifiableSet(EnumSet.of(URIishField.USER,
123 					URIishField.PASS, URIishField.PORT));
124 		}
125 
126 		@Override
127 		public int getDefaultPort() {
128 			return 22;
129 		}
130 
131 		@Override
132 		public Transport open(URIish uri, Repository local, String remoteName)
133 				throws NotSupportedException {
134 			return new TransportSftp(local, uri);
135 		}
136 	};
137 
138 	TransportSftp(final Repository local, final URIish uri) {
139 		super(local, uri);
140 	}
141 
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 	@Override
151 	public PushConnection openPush() throws TransportException {
152 		final SftpObjectDB c = new SftpObjectDB(uri.getPath());
153 		final WalkPushConnection r = new WalkPushConnection(this, c);
154 		r.available(c.readAdvertisedRefs());
155 		return r;
156 	}
157 
158 	ChannelSftp newSftp() throws TransportException {
159 		final int tms = getTimeout() > 0 ? getTimeout() * 1000 : 0;
160 		try {
161 			// @TODO: Fix so that this operation is generic and casting to
162 			// JschSession is no longer necessary.
163 			final Channel channel = ((JschSession) getSession())
164 					.getSftpChannel();
165 			channel.connect(tms);
166 			return (ChannelSftp) channel;
167 		} catch (JSchException je) {
168 			throw new TransportException(uri, je.getMessage(), je);
169 		}
170 	}
171 
172 	class SftpObjectDB extends WalkRemoteObjectDatabase {
173 		private final String objectsPath;
174 
175 		private ChannelSftp ftp;
176 
177 		SftpObjectDB(String path) throws TransportException {
178 			if (path.startsWith("/~")) //$NON-NLS-1$
179 				path = path.substring(1);
180 			if (path.startsWith("~/")) //$NON-NLS-1$
181 				path = path.substring(2);
182 			try {
183 				ftp = newSftp();
184 				ftp.cd(path);
185 				ftp.cd("objects"); //$NON-NLS-1$
186 				objectsPath = ftp.pwd();
187 			} catch (TransportException err) {
188 				close();
189 				throw err;
190 			} catch (SftpException je) {
191 				throw new TransportException(MessageFormat.format(
192 						JGitText.get().cannotEnterObjectsPath, path,
193 						je.getMessage()), je);
194 			}
195 		}
196 
197 		SftpObjectDB(final SftpObjectDB parent, final String p)
198 				throws TransportException {
199 			try {
200 				ftp = newSftp();
201 				ftp.cd(parent.objectsPath);
202 				ftp.cd(p);
203 				objectsPath = ftp.pwd();
204 			} catch (TransportException err) {
205 				close();
206 				throw err;
207 			} catch (SftpException je) {
208 				throw new TransportException(MessageFormat.format(
209 						JGitText.get().cannotEnterPathFromParent, p,
210 						parent.objectsPath, je.getMessage()), je);
211 			}
212 		}
213 
214 		@Override
215 		URIish getURI() {
216 			return uri.setPath(objectsPath);
217 		}
218 
219 		@Override
220 		Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException {
221 			try {
222 				return readAlternates(INFO_ALTERNATES);
223 			} catch (FileNotFoundException err) {
224 				return null;
225 			}
226 		}
227 
228 		@Override
229 		WalkRemoteObjectDatabase openAlternate(final String location)
230 				throws IOException {
231 			return new SftpObjectDB(this, location);
232 		}
233 
234 		@Override
235 		Collection<String> getPackNames() throws IOException {
236 			final List<String> packs = new ArrayList<>();
237 			try {
238 				@SuppressWarnings("unchecked")
239 				final Collection<ChannelSftp.LsEntry> list = ftp.ls("pack"); //$NON-NLS-1$
240 				final HashMap<String, ChannelSftp.LsEntry> files;
241 				final HashMap<String, Integer> mtimes;
242 
243 				files = new HashMap<>();
244 				mtimes = new HashMap<>();
245 
246 				for (final ChannelSftp.LsEntry ent : list)
247 					files.put(ent.getFilename(), ent);
248 				for (final ChannelSftp.LsEntry ent : list) {
249 					final String n = ent.getFilename();
250 					if (!n.startsWith("pack-") || !n.endsWith(".pack")) //$NON-NLS-1$ //$NON-NLS-2$
251 						continue;
252 
253 					final String in = n.substring(0, n.length() - 5) + ".idx"; //$NON-NLS-1$
254 					if (!files.containsKey(in))
255 						continue;
256 
257 					mtimes.put(n, Integer.valueOf(ent.getAttrs().getMTime()));
258 					packs.add(n);
259 				}
260 
261 				Collections.sort(packs, new Comparator<String>() {
262 					@Override
263 					public int compare(final String o1, final String o2) {
264 						return mtimes.get(o2).intValue()
265 								- mtimes.get(o1).intValue();
266 					}
267 				});
268 			} catch (SftpException je) {
269 				throw new TransportException(
270 						MessageFormat.format(JGitText.get().cannotListPackPath,
271 								objectsPath, je.getMessage()),
272 						je);
273 			}
274 			return packs;
275 		}
276 
277 		@Override
278 		FileStream open(final String path) throws IOException {
279 			try {
280 				final SftpATTRS a = ftp.lstat(path);
281 				return new FileStream(ftp.get(path), a.getSize());
282 			} catch (SftpException je) {
283 				if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
284 					throw new FileNotFoundException(path);
285 				throw new TransportException(MessageFormat.format(
286 						JGitText.get().cannotGetObjectsPath, objectsPath, path,
287 						je.getMessage()), je);
288 			}
289 		}
290 
291 		@Override
292 		void deleteFile(final String path) throws IOException {
293 			try {
294 				ftp.rm(path);
295 			} catch (SftpException je) {
296 				if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
297 					return;
298 				throw new TransportException(MessageFormat.format(
299 						JGitText.get().cannotDeleteObjectsPath, objectsPath,
300 						path, je.getMessage()), je);
301 			}
302 
303 			// Prune any now empty directories.
304 			//
305 			String dir = path;
306 			int s = dir.lastIndexOf('/');
307 			while (s > 0) {
308 				try {
309 					dir = dir.substring(0, s);
310 					ftp.rmdir(dir);
311 					s = dir.lastIndexOf('/');
312 				} catch (SftpException je) {
313 					// If we cannot delete it, leave it alone. It may have
314 					// entries still in it, or maybe we lack write access on
315 					// the parent. Either way it isn't a fatal error.
316 					//
317 					break;
318 				}
319 			}
320 		}
321 
322 		@Override
323 		OutputStream writeFile(final String path,
324 				final ProgressMonitor monitor, final String monitorTask)
325 				throws IOException {
326 			try {
327 				return ftp.put(path);
328 			} catch (SftpException je) {
329 				if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
330 					mkdir_p(path);
331 					try {
332 						return ftp.put(path);
333 					} catch (SftpException je2) {
334 						je = je2;
335 					}
336 				}
337 
338 				throw new TransportException(MessageFormat.format(
339 						JGitText.get().cannotWriteObjectsPath, objectsPath,
340 						path, je.getMessage()), je);
341 			}
342 		}
343 
344 		@Override
345 		void writeFile(final String path, final byte[] data) throws IOException {
346 			final String lock = path + LOCK_SUFFIX;
347 			try {
348 				super.writeFile(lock, data);
349 				try {
350 					ftp.rename(lock, path);
351 				} catch (SftpException je) {
352 					throw new TransportException(MessageFormat.format(
353 							JGitText.get().cannotWriteObjectsPath, objectsPath,
354 							path, je.getMessage()), je);
355 				}
356 			} catch (IOException err) {
357 				try {
358 					ftp.rm(lock);
359 				} catch (SftpException e) {
360 					// Ignore deletion failure, we are already
361 					// failing anyway.
362 				}
363 				throw err;
364 			}
365 		}
366 
367 		private void mkdir_p(String path) throws IOException {
368 			final int s = path.lastIndexOf('/');
369 			if (s <= 0)
370 				return;
371 
372 			path = path.substring(0, s);
373 			try {
374 				ftp.mkdir(path);
375 			} catch (SftpException je) {
376 				if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
377 					mkdir_p(path);
378 					try {
379 						ftp.mkdir(path);
380 						return;
381 					} catch (SftpException je2) {
382 						je = je2;
383 					}
384 				}
385 
386 				throw new TransportException(MessageFormat.format(
387 						JGitText.get().cannotMkdirObjectPath, objectsPath, path,
388 						je.getMessage()), je);
389 			}
390 		}
391 
392 		Map<String, Ref> readAdvertisedRefs() throws TransportException {
393 			final TreeMap<String, Ref> avail = new TreeMap<>();
394 			readPackedRefs(avail);
395 			readRef(avail, ROOT_DIR + Constants.HEAD, Constants.HEAD);
396 			readLooseRefs(avail, ROOT_DIR + "refs", "refs/"); //$NON-NLS-1$ //$NON-NLS-2$
397 			return avail;
398 		}
399 
400 		@SuppressWarnings("unchecked")
401 		private void readLooseRefs(final TreeMap<String, Ref> avail,
402 				final String dir, final String prefix)
403 				throws TransportException {
404 			final Collection<ChannelSftp.LsEntry> list;
405 			try {
406 				list = ftp.ls(dir);
407 			} catch (SftpException je) {
408 				throw new TransportException(MessageFormat.format(
409 						JGitText.get().cannotListObjectsPath, objectsPath, dir,
410 						je.getMessage()), je);
411 			}
412 
413 			for (final ChannelSftp.LsEntry ent : list) {
414 				final String n = ent.getFilename();
415 				if (".".equals(n) || "..".equals(n)) //$NON-NLS-1$ //$NON-NLS-2$
416 					continue;
417 
418 				final String nPath = dir + "/" + n; //$NON-NLS-1$
419 				if (ent.getAttrs().isDir())
420 					readLooseRefs(avail, nPath, prefix + n + "/"); //$NON-NLS-1$
421 				else
422 					readRef(avail, nPath, prefix + n);
423 			}
424 		}
425 
426 		private Ref readRef(final TreeMap<String, Ref> avail,
427 				final String path, final String name) throws TransportException {
428 			final String line;
429 			try {
430 				final BufferedReader br = openReader(path);
431 				try {
432 					line = br.readLine();
433 				} finally {
434 					br.close();
435 				}
436 			} catch (FileNotFoundException noRef) {
437 				return null;
438 			} catch (IOException err) {
439 				throw new TransportException(MessageFormat.format(
440 						JGitText.get().cannotReadObjectsPath, objectsPath, path,
441 						err.getMessage()), err);
442 			}
443 
444 			if (line == null)
445 				throw new TransportException(
446 						MessageFormat.format(JGitText.get().emptyRef, name));
447 
448 			if (line.startsWith("ref: ")) { //$NON-NLS-1$
449 				final String target = line.substring("ref: ".length()); //$NON-NLS-1$
450 				Ref r = avail.get(target);
451 				if (r == null)
452 					r = readRef(avail, ROOT_DIR + target, target);
453 				if (r == null)
454 					r = new ObjectIdRef.Unpeeled(Ref.Storage.NEW, target, null);
455 				r = new SymbolicRef(name, r);
456 				avail.put(r.getName(), r);
457 				return r;
458 			}
459 
460 			if (ObjectId.isId(line)) {
461 				final Ref r = new ObjectIdRef.Unpeeled(loose(avail.get(name)),
462 						name, ObjectId.fromString(line));
463 				avail.put(r.getName(), r);
464 				return r;
465 			}
466 
467 			throw new TransportException(
468 					MessageFormat.format(JGitText.get().badRef, name, line));
469 		}
470 
471 		private Storage loose(final Ref r) {
472 			if (r != null && r.getStorage() == Storage.PACKED)
473 				return Storage.LOOSE_PACKED;
474 			return Storage.LOOSE;
475 		}
476 
477 		@Override
478 		void close() {
479 			if (ftp != null) {
480 				try {
481 					if (ftp.isConnected())
482 						ftp.disconnect();
483 				} finally {
484 					ftp = null;
485 				}
486 			}
487 		}
488 	}
489 }