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