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