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.INFO_ALTERNATES;
47  import static org.eclipse.jgit.lib.Constants.LOCK_SUFFIX;
48  import static org.eclipse.jgit.lib.Constants.OBJECTS;
49  
50  import java.io.BufferedReader;
51  import java.io.FileNotFoundException;
52  import java.io.IOException;
53  import java.io.OutputStream;
54  import java.text.MessageFormat;
55  import java.util.ArrayList;
56  import java.util.Collection;
57  import java.util.Collections;
58  import java.util.EnumSet;
59  import java.util.HashMap;
60  import java.util.List;
61  import java.util.Map;
62  import java.util.Set;
63  import java.util.TreeMap;
64  import java.util.concurrent.TimeUnit;
65  import java.util.stream.Collectors;
66  
67  import org.eclipse.jgit.errors.NotSupportedException;
68  import org.eclipse.jgit.errors.TransportException;
69  import org.eclipse.jgit.internal.JGitText;
70  import org.eclipse.jgit.lib.Constants;
71  import org.eclipse.jgit.lib.ObjectId;
72  import org.eclipse.jgit.lib.ObjectIdRef;
73  import org.eclipse.jgit.lib.ProgressMonitor;
74  import org.eclipse.jgit.lib.Ref;
75  import org.eclipse.jgit.lib.Ref.Storage;
76  import org.eclipse.jgit.lib.Repository;
77  import org.eclipse.jgit.lib.SymbolicRef;
78  
79  /**
80   * Transport over the non-Git aware SFTP (SSH based FTP) protocol.
81   * <p>
82   * The SFTP transport does not require any specialized Git support on the remote
83   * (server side) repository. Object files are retrieved directly through secure
84   * shell's FTP protocol, making it possible to copy objects from a remote
85   * repository that is available over SSH, but whose remote host does not have
86   * Git installed.
87   * <p>
88   * Unlike the HTTP variant (see
89   * {@link org.eclipse.jgit.transport.TransportHttp}) we rely upon being able to
90   * 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 remote
93   * 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 TransportProtocoll.html#TransportProtocol">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(Repository local, URIish uri) {
137 		super(local, uri);
138 	}
139 
140 	/** {@inheritDoc} */
141 	@Override
142 	public FetchConnection openFetch() throws TransportException {
143 		final SftpObjectDB c = new SftpObjectDB(uri.getPath());
144 		final WalkFetchConnectionConnection.html#WalkFetchConnection">WalkFetchConnection r = new WalkFetchConnection(this, c);
145 		r.available(c.readAdvertisedRefs());
146 		return r;
147 	}
148 
149 	/** {@inheritDoc} */
150 	@Override
151 	public PushConnection openPush() throws TransportException {
152 		final SftpObjectDB c = new SftpObjectDB(uri.getPath());
153 		final WalkPushConnectionConnection.html#WalkPushConnection">WalkPushConnection r = new WalkPushConnection(this, c);
154 		r.available(c.readAdvertisedRefs());
155 		return r;
156 	}
157 
158 	FtpChannel newSftp() throws IOException {
159 		FtpChannel channel = getSession().getFtpChannel();
160 		channel.connect(getTimeout(), TimeUnit.SECONDS);
161 		return channel;
162 	}
163 
164 	class SftpObjectDB extends WalkRemoteObjectDatabase {
165 		private final String objectsPath;
166 
167 		private FtpChannel ftp;
168 
169 		SftpObjectDB(String path) throws TransportException {
170 			if (path.startsWith("/~")) //$NON-NLS-1$
171 				path = path.substring(1);
172 			if (path.startsWith("~/")) //$NON-NLS-1$
173 				path = path.substring(2);
174 			try {
175 				ftp = newSftp();
176 				ftp.cd(path);
177 				ftp.cd(OBJECTS);
178 				objectsPath = ftp.pwd();
179 			} catch (FtpChannel.FtpException f) {
180 				throw new TransportException(MessageFormat.format(
181 						JGitText.get().cannotEnterObjectsPath, path,
182 						f.getMessage()), f);
183 			} catch (IOException ioe) {
184 				close();
185 				throw new TransportException(uri, ioe.getMessage(), ioe);
186 			}
187 		}
188 
189 		SftpObjectDB(SftpObjectDB parent, String p)
190 				throws TransportException {
191 			try {
192 				ftp = newSftp();
193 				ftp.cd(parent.objectsPath);
194 				ftp.cd(p);
195 				objectsPath = ftp.pwd();
196 			} catch (FtpChannel.FtpException f) {
197 				throw new TransportException(MessageFormat.format(
198 						JGitText.get().cannotEnterPathFromParent, p,
199 						parent.objectsPath, f.getMessage()), f);
200 			} catch (IOException ioe) {
201 				close();
202 				throw new TransportException(uri, ioe.getMessage(), ioe);
203 			}
204 		}
205 
206 		@Override
207 		URIish getURI() {
208 			return uri.setPath(objectsPath);
209 		}
210 
211 		@Override
212 		Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException {
213 			try {
214 				return readAlternates(INFO_ALTERNATES);
215 			} catch (FileNotFoundException err) {
216 				return null;
217 			}
218 		}
219 
220 		@Override
221 		WalkRemoteObjectDatabase openAlternate(String location)
222 				throws IOException {
223 			return new SftpObjectDB(this, location);
224 		}
225 
226 		@Override
227 		Collection<String> getPackNames() throws IOException {
228 			final List<String> packs = new ArrayList<>();
229 			try {
230 				Collection<FtpChannel.DirEntry> list = ftp.ls("pack"); //$NON-NLS-1$
231 				Set<String> files = list.stream()
232 						.map(FtpChannel.DirEntry::getFilename)
233 						.collect(Collectors.toSet());
234 				HashMap<String, Long> mtimes = new HashMap<>();
235 
236 				for (FtpChannel.DirEntry ent : list) {
237 					String n = ent.getFilename();
238 					if (!n.startsWith("pack-") || !n.endsWith(".pack")) { //$NON-NLS-1$ //$NON-NLS-2$
239 						continue;
240 					}
241 					String in = n.substring(0, n.length() - 5) + ".idx"; //$NON-NLS-1$
242 					if (!files.contains(in)) {
243 						continue;
244 					}
245 					mtimes.put(n, Long.valueOf(ent.getModifiedTime()));
246 					packs.add(n);
247 				}
248 
249 				Collections.sort(packs,
250 						(o1, o2) -> mtimes.get(o2).compareTo(mtimes.get(o1)));
251 			} catch (FtpChannel.FtpException f) {
252 				throw new TransportException(
253 						MessageFormat.format(JGitText.get().cannotListPackPath,
254 								objectsPath, f.getMessage()),
255 						f);
256 			}
257 			return packs;
258 		}
259 
260 		@Override
261 		FileStream open(String path) throws IOException {
262 			try {
263 				return new FileStream(ftp.get(path));
264 			} catch (FtpChannel.FtpException f) {
265 				if (f.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) {
266 					throw new FileNotFoundException(path);
267 				}
268 				throw new TransportException(MessageFormat.format(
269 						JGitText.get().cannotGetObjectsPath, objectsPath, path,
270 						f.getMessage()), f);
271 			}
272 		}
273 
274 		@Override
275 		void deleteFile(String path) throws IOException {
276 			try {
277 				ftp.delete(path);
278 			} catch (FtpChannel.FtpException f) {
279 				throw new TransportException(MessageFormat.format(
280 						JGitText.get().cannotDeleteObjectsPath, objectsPath,
281 						path, f.getMessage()), f);
282 			}
283 
284 			// Prune any now empty directories.
285 			//
286 			String dir = path;
287 			int s = dir.lastIndexOf('/');
288 			while (s > 0) {
289 				try {
290 					dir = dir.substring(0, s);
291 					ftp.rmdir(dir);
292 					s = dir.lastIndexOf('/');
293 				} catch (IOException je) {
294 					// If we cannot delete it, leave it alone. It may have
295 					// entries still in it, or maybe we lack write access on
296 					// the parent. Either way it isn't a fatal error.
297 					//
298 					break;
299 				}
300 			}
301 		}
302 
303 		@Override
304 		OutputStream writeFile(String path, ProgressMonitor monitor,
305 				String monitorTask) throws IOException {
306 			Throwable err = null;
307 			try {
308 				return ftp.put(path);
309 			} catch (FileNotFoundException e) {
310 				mkdir_p(path);
311 			} catch (FtpChannel.FtpException je) {
312 				if (je.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) {
313 					mkdir_p(path);
314 				} else {
315 					err = je;
316 				}
317 			}
318 			if (err == null) {
319 				try {
320 					return ftp.put(path);
321 				} catch (IOException e) {
322 					err = e;
323 				}
324 			}
325 			throw new TransportException(
326 					MessageFormat.format(JGitText.get().cannotWriteObjectsPath,
327 							objectsPath, path, err.getMessage()),
328 					err);
329 		}
330 
331 		@Override
332 		void writeFile(String path, byte[] data) throws IOException {
333 			final String lock = path + LOCK_SUFFIX;
334 			try {
335 				super.writeFile(lock, data);
336 				try {
337 					ftp.rename(lock, path);
338 				} catch (IOException e) {
339 					throw new TransportException(MessageFormat.format(
340 							JGitText.get().cannotWriteObjectsPath, objectsPath,
341 							path, e.getMessage()), e);
342 				}
343 			} catch (IOException err) {
344 				try {
345 					ftp.rm(lock);
346 				} catch (IOException e) {
347 					// Ignore deletion failure, we are already
348 					// failing anyway.
349 				}
350 				throw err;
351 			}
352 		}
353 
354 		private void mkdir_p(String path) throws IOException {
355 			final int s = path.lastIndexOf('/');
356 			if (s <= 0)
357 				return;
358 
359 			path = path.substring(0, s);
360 			Throwable err = null;
361 			try {
362 				ftp.mkdir(path);
363 				return;
364 			} catch (FileNotFoundException f) {
365 				mkdir_p(path);
366 			} catch (FtpChannel.FtpException je) {
367 				if (je.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) {
368 					mkdir_p(path);
369 				} else {
370 					err = je;
371 				}
372 			}
373 			if (err == null) {
374 				try {
375 					ftp.mkdir(path);
376 					return;
377 				} catch (IOException e) {
378 					err = e;
379 				}
380 			}
381 			throw new TransportException(MessageFormat.format(
382 						JGitText.get().cannotMkdirObjectPath, objectsPath, path,
383 					err.getMessage()), err);
384 		}
385 
386 		Map<String, Ref> readAdvertisedRefs() throws TransportException {
387 			final TreeMap<String, Ref> avail = new TreeMap<>();
388 			readPackedRefs(avail);
389 			readRef(avail, ROOT_DIR + Constants.HEAD, Constants.HEAD);
390 			readLooseRefs(avail, ROOT_DIR + "refs", "refs/"); //$NON-NLS-1$ //$NON-NLS-2$
391 			return avail;
392 		}
393 
394 		private void readLooseRefs(TreeMap<String, Ref> avail, String dir,
395 				String prefix) throws TransportException {
396 			final Collection<FtpChannel.DirEntry> list;
397 			try {
398 				list = ftp.ls(dir);
399 			} catch (IOException e) {
400 				throw new TransportException(MessageFormat.format(
401 						JGitText.get().cannotListObjectsPath, objectsPath, dir,
402 						e.getMessage()), e);
403 			}
404 
405 			for (FtpChannel.DirEntry ent : list) {
406 				String n = ent.getFilename();
407 				if (".".equals(n) || "..".equals(n)) //$NON-NLS-1$ //$NON-NLS-2$
408 					continue;
409 
410 				String nPath = dir + "/" + n; //$NON-NLS-1$
411 				if (ent.isDirectory()) {
412 					readLooseRefs(avail, nPath, prefix + n + "/"); //$NON-NLS-1$
413 				} else {
414 					readRef(avail, nPath, prefix + n);
415 				}
416 			}
417 		}
418 
419 		private Ref readRef(TreeMap<String, Ref> avail, String path,
420 				String name) throws TransportException {
421 			final String line;
422 			try (BufferedReader br = openReader(path)) {
423 				line = br.readLine();
424 			} catch (FileNotFoundException noRef) {
425 				return null;
426 			} catch (IOException err) {
427 				throw new TransportException(MessageFormat.format(
428 						JGitText.get().cannotReadObjectsPath, objectsPath, path,
429 						err.getMessage()), err);
430 			}
431 
432 			if (line == null) {
433 				throw new TransportException(
434 						MessageFormat.format(JGitText.get().emptyRef, name));
435 			}
436 			if (line.startsWith("ref: ")) { //$NON-NLS-1$
437 				final String target = line.substring("ref: ".length()); //$NON-NLS-1$
438 				Ref r = avail.get(target);
439 				if (r == null)
440 					r = readRef(avail, ROOT_DIR + target, target);
441 				if (r == null)
442 					r = new ObjectIdRef.Unpeeled(Ref.Storage.NEW, target, null);
443 				r = new SymbolicRef(name, r);
444 				avail.put(r.getName(), r);
445 				return r;
446 			}
447 
448 			if (ObjectId.isId(line)) {
449 				final Ref r = new ObjectIdRef.Unpeeled(loose(avail.get(name)),
450 						name, ObjectId.fromString(line));
451 				avail.put(r.getName(), r);
452 				return r;
453 			}
454 
455 			throw new TransportException(
456 					MessageFormat.format(JGitText.get().badRef, name, line));
457 		}
458 
459 		private Storage loose(Ref r) {
460 			if (r != null && r.getStorage() == Storage.PACKED) {
461 				return Storage.LOOSE_PACKED;
462 			}
463 			return Storage.LOOSE;
464 		}
465 
466 		@Override
467 		void close() {
468 			if (ftp != null) {
469 				try {
470 					if (ftp.isConnected()) {
471 						ftp.disconnect();
472 					}
473 				} finally {
474 					ftp = null;
475 				}
476 			}
477 		}
478 	}
479 }