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