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