TransportSftp.java

  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. package org.eclipse.jgit.transport;

  11. import static org.eclipse.jgit.lib.Constants.INFO_ALTERNATES;
  12. import static org.eclipse.jgit.lib.Constants.LOCK_SUFFIX;
  13. import static org.eclipse.jgit.lib.Constants.OBJECTS;

  14. import java.io.BufferedReader;
  15. import java.io.FileNotFoundException;
  16. import java.io.IOException;
  17. import java.io.OutputStream;
  18. import java.text.MessageFormat;
  19. import java.util.ArrayList;
  20. import java.util.Collection;
  21. import java.util.Collections;
  22. import java.util.EnumSet;
  23. import java.util.HashMap;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Set;
  27. import java.util.TreeMap;
  28. import java.util.concurrent.TimeUnit;
  29. import java.util.stream.Collectors;

  30. import org.eclipse.jgit.errors.NotSupportedException;
  31. import org.eclipse.jgit.errors.TransportException;
  32. import org.eclipse.jgit.internal.JGitText;
  33. import org.eclipse.jgit.lib.Constants;
  34. import org.eclipse.jgit.lib.ObjectId;
  35. import org.eclipse.jgit.lib.ObjectIdRef;
  36. import org.eclipse.jgit.lib.ProgressMonitor;
  37. import org.eclipse.jgit.lib.Ref;
  38. import org.eclipse.jgit.lib.Ref.Storage;
  39. import org.eclipse.jgit.lib.Repository;
  40. import org.eclipse.jgit.lib.SymbolicRef;

  41. /**
  42.  * Transport over the non-Git aware SFTP (SSH based FTP) protocol.
  43.  * <p>
  44.  * The SFTP transport does not require any specialized Git support on the remote
  45.  * (server side) repository. Object files are retrieved directly through secure
  46.  * shell's FTP protocol, making it possible to copy objects from a remote
  47.  * repository that is available over SSH, but whose remote host does not have
  48.  * Git installed.
  49.  * <p>
  50.  * Unlike the HTTP variant (see
  51.  * {@link org.eclipse.jgit.transport.TransportHttp}) we rely upon being able to
  52.  * list files in directories, as the SFTP protocol supports this function. By
  53.  * listing files through SFTP we can avoid needing to have current
  54.  * <code>objects/info/packs</code> or <code>info/refs</code> files on the remote
  55.  * repository and access the data directly, much as Git itself would.
  56.  * <p>
  57.  * Concurrent pushing over this transport is not supported. Multiple concurrent
  58.  * push operations may cause confusion in the repository state.
  59.  *
  60.  * @see WalkFetchConnection
  61.  */
  62. public class TransportSftp extends SshTransport implements WalkTransport {
  63.     static final TransportProtocol PROTO_SFTP = new TransportProtocol() {
  64.         @Override
  65.         public String getName() {
  66.             return JGitText.get().transportProtoSFTP;
  67.         }

  68.         @Override
  69.         public Set<String> getSchemes() {
  70.             return Collections.singleton("sftp"); //$NON-NLS-1$
  71.         }

  72.         @Override
  73.         public Set<URIishField> getRequiredFields() {
  74.             return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST,
  75.                     URIishField.PATH));
  76.         }

  77.         @Override
  78.         public Set<URIishField> getOptionalFields() {
  79.             return Collections.unmodifiableSet(EnumSet.of(URIishField.USER,
  80.                     URIishField.PASS, URIishField.PORT));
  81.         }

  82.         @Override
  83.         public int getDefaultPort() {
  84.             return 22;
  85.         }

  86.         @Override
  87.         public Transport open(URIish uri, Repository local, String remoteName)
  88.                 throws NotSupportedException {
  89.             return new TransportSftp(local, uri);
  90.         }
  91.     };

  92.     TransportSftp(Repository local, URIish uri) {
  93.         super(local, uri);
  94.     }

  95.     /** {@inheritDoc} */
  96.     @Override
  97.     public FetchConnection openFetch() throws TransportException {
  98.         final SftpObjectDB c = new SftpObjectDB(uri.getPath());
  99.         final WalkFetchConnection r = new WalkFetchConnection(this, c);
  100.         r.available(c.readAdvertisedRefs());
  101.         return r;
  102.     }

  103.     /** {@inheritDoc} */
  104.     @Override
  105.     public PushConnection openPush() throws TransportException {
  106.         final SftpObjectDB c = new SftpObjectDB(uri.getPath());
  107.         final WalkPushConnection r = new WalkPushConnection(this, c);
  108.         r.available(c.readAdvertisedRefs());
  109.         return r;
  110.     }

  111.     FtpChannel newSftp() throws IOException {
  112.         FtpChannel channel = getSession().getFtpChannel();
  113.         channel.connect(getTimeout(), TimeUnit.SECONDS);
  114.         return channel;
  115.     }

  116.     class SftpObjectDB extends WalkRemoteObjectDatabase {
  117.         private final String objectsPath;

  118.         private FtpChannel ftp;

  119.         SftpObjectDB(String path) throws TransportException {
  120.             if (path.startsWith("/~")) //$NON-NLS-1$
  121.                 path = path.substring(1);
  122.             if (path.startsWith("~/")) //$NON-NLS-1$
  123.                 path = path.substring(2);
  124.             try {
  125.                 ftp = newSftp();
  126.                 ftp.cd(path);
  127.                 ftp.cd(OBJECTS);
  128.                 objectsPath = ftp.pwd();
  129.             } catch (FtpChannel.FtpException f) {
  130.                 throw new TransportException(MessageFormat.format(
  131.                         JGitText.get().cannotEnterObjectsPath, path,
  132.                         f.getMessage()), f);
  133.             } catch (IOException ioe) {
  134.                 close();
  135.                 throw new TransportException(uri, ioe.getMessage(), ioe);
  136.             }
  137.         }

  138.         SftpObjectDB(SftpObjectDB parent, String p)
  139.                 throws TransportException {
  140.             try {
  141.                 ftp = newSftp();
  142.                 ftp.cd(parent.objectsPath);
  143.                 ftp.cd(p);
  144.                 objectsPath = ftp.pwd();
  145.             } catch (FtpChannel.FtpException f) {
  146.                 throw new TransportException(MessageFormat.format(
  147.                         JGitText.get().cannotEnterPathFromParent, p,
  148.                         parent.objectsPath, f.getMessage()), f);
  149.             } catch (IOException ioe) {
  150.                 close();
  151.                 throw new TransportException(uri, ioe.getMessage(), ioe);
  152.             }
  153.         }

  154.         @Override
  155.         URIish getURI() {
  156.             return uri.setPath(objectsPath);
  157.         }

  158.         @Override
  159.         Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException {
  160.             try {
  161.                 return readAlternates(INFO_ALTERNATES);
  162.             } catch (FileNotFoundException err) {
  163.                 return null;
  164.             }
  165.         }

  166.         @Override
  167.         WalkRemoteObjectDatabase openAlternate(String location)
  168.                 throws IOException {
  169.             return new SftpObjectDB(this, location);
  170.         }

  171.         @Override
  172.         Collection<String> getPackNames() throws IOException {
  173.             final List<String> packs = new ArrayList<>();
  174.             try {
  175.                 Collection<FtpChannel.DirEntry> list = ftp.ls("pack"); //$NON-NLS-1$
  176.                 Set<String> files = list.stream()
  177.                         .map(FtpChannel.DirEntry::getFilename)
  178.                         .collect(Collectors.toSet());
  179.                 HashMap<String, Long> mtimes = new HashMap<>();

  180.                 for (FtpChannel.DirEntry ent : list) {
  181.                     String n = ent.getFilename();
  182.                     if (!n.startsWith("pack-") || !n.endsWith(".pack")) { //$NON-NLS-1$ //$NON-NLS-2$
  183.                         continue;
  184.                     }
  185.                     String in = n.substring(0, n.length() - 5) + ".idx"; //$NON-NLS-1$
  186.                     if (!files.contains(in)) {
  187.                         continue;
  188.                     }
  189.                     mtimes.put(n, Long.valueOf(ent.getModifiedTime()));
  190.                     packs.add(n);
  191.                 }

  192.                 Collections.sort(packs,
  193.                         (o1, o2) -> mtimes.get(o2).compareTo(mtimes.get(o1)));
  194.             } catch (FtpChannel.FtpException f) {
  195.                 throw new TransportException(
  196.                         MessageFormat.format(JGitText.get().cannotListPackPath,
  197.                                 objectsPath, f.getMessage()),
  198.                         f);
  199.             }
  200.             return packs;
  201.         }

  202.         @Override
  203.         FileStream open(String path) throws IOException {
  204.             try {
  205.                 return new FileStream(ftp.get(path));
  206.             } catch (FtpChannel.FtpException f) {
  207.                 if (f.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) {
  208.                     throw new FileNotFoundException(path);
  209.                 }
  210.                 throw new TransportException(MessageFormat.format(
  211.                         JGitText.get().cannotGetObjectsPath, objectsPath, path,
  212.                         f.getMessage()), f);
  213.             }
  214.         }

  215.         @Override
  216.         void deleteFile(String path) throws IOException {
  217.             try {
  218.                 ftp.delete(path);
  219.             } catch (FtpChannel.FtpException f) {
  220.                 throw new TransportException(MessageFormat.format(
  221.                         JGitText.get().cannotDeleteObjectsPath, objectsPath,
  222.                         path, f.getMessage()), f);
  223.             }

  224.             // Prune any now empty directories.
  225.             //
  226.             String dir = path;
  227.             int s = dir.lastIndexOf('/');
  228.             while (s > 0) {
  229.                 try {
  230.                     dir = dir.substring(0, s);
  231.                     ftp.rmdir(dir);
  232.                     s = dir.lastIndexOf('/');
  233.                 } catch (IOException je) {
  234.                     // If we cannot delete it, leave it alone. It may have
  235.                     // entries still in it, or maybe we lack write access on
  236.                     // the parent. Either way it isn't a fatal error.
  237.                     //
  238.                     break;
  239.                 }
  240.             }
  241.         }

  242.         @Override
  243.         OutputStream writeFile(String path, ProgressMonitor monitor,
  244.                 String monitorTask) throws IOException {
  245.             Throwable err = null;
  246.             try {
  247.                 return ftp.put(path);
  248.             } catch (FileNotFoundException e) {
  249.                 mkdir_p(path);
  250.             } catch (FtpChannel.FtpException je) {
  251.                 if (je.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) {
  252.                     mkdir_p(path);
  253.                 } else {
  254.                     err = je;
  255.                 }
  256.             }
  257.             if (err == null) {
  258.                 try {
  259.                     return ftp.put(path);
  260.                 } catch (IOException e) {
  261.                     err = e;
  262.                 }
  263.             }
  264.             throw new TransportException(
  265.                     MessageFormat.format(JGitText.get().cannotWriteObjectsPath,
  266.                             objectsPath, path, err.getMessage()),
  267.                     err);
  268.         }

  269.         @Override
  270.         void writeFile(String path, byte[] data) throws IOException {
  271.             final String lock = path + LOCK_SUFFIX;
  272.             try {
  273.                 super.writeFile(lock, data);
  274.                 try {
  275.                     ftp.rename(lock, path);
  276.                 } catch (IOException e) {
  277.                     throw new TransportException(MessageFormat.format(
  278.                             JGitText.get().cannotWriteObjectsPath, objectsPath,
  279.                             path, e.getMessage()), e);
  280.                 }
  281.             } catch (IOException err) {
  282.                 try {
  283.                     ftp.rm(lock);
  284.                 } catch (IOException e) {
  285.                     // Ignore deletion failure, we are already
  286.                     // failing anyway.
  287.                 }
  288.                 throw err;
  289.             }
  290.         }

  291.         private void mkdir_p(String path) throws IOException {
  292.             final int s = path.lastIndexOf('/');
  293.             if (s <= 0)
  294.                 return;

  295.             path = path.substring(0, s);
  296.             Throwable err = null;
  297.             try {
  298.                 ftp.mkdir(path);
  299.                 return;
  300.             } catch (FileNotFoundException f) {
  301.                 mkdir_p(path);
  302.             } catch (FtpChannel.FtpException je) {
  303.                 if (je.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) {
  304.                     mkdir_p(path);
  305.                 } else {
  306.                     err = je;
  307.                 }
  308.             }
  309.             if (err == null) {
  310.                 try {
  311.                     ftp.mkdir(path);
  312.                     return;
  313.                 } catch (IOException e) {
  314.                     err = e;
  315.                 }
  316.             }
  317.             throw new TransportException(MessageFormat.format(
  318.                         JGitText.get().cannotMkdirObjectPath, objectsPath, path,
  319.                     err.getMessage()), err);
  320.         }

  321.         Map<String, Ref> readAdvertisedRefs() throws TransportException {
  322.             final TreeMap<String, Ref> avail = new TreeMap<>();
  323.             readPackedRefs(avail);
  324.             readRef(avail, ROOT_DIR + Constants.HEAD, Constants.HEAD);
  325.             readLooseRefs(avail, ROOT_DIR + "refs", "refs/"); //$NON-NLS-1$ //$NON-NLS-2$
  326.             return avail;
  327.         }

  328.         private void readLooseRefs(TreeMap<String, Ref> avail, String dir,
  329.                 String prefix) throws TransportException {
  330.             final Collection<FtpChannel.DirEntry> list;
  331.             try {
  332.                 list = ftp.ls(dir);
  333.             } catch (IOException e) {
  334.                 throw new TransportException(MessageFormat.format(
  335.                         JGitText.get().cannotListObjectsPath, objectsPath, dir,
  336.                         e.getMessage()), e);
  337.             }

  338.             for (FtpChannel.DirEntry ent : list) {
  339.                 String n = ent.getFilename();
  340.                 if (".".equals(n) || "..".equals(n)) //$NON-NLS-1$ //$NON-NLS-2$
  341.                     continue;

  342.                 String nPath = dir + "/" + n; //$NON-NLS-1$
  343.                 if (ent.isDirectory()) {
  344.                     readLooseRefs(avail, nPath, prefix + n + "/"); //$NON-NLS-1$
  345.                 } else {
  346.                     readRef(avail, nPath, prefix + n);
  347.                 }
  348.             }
  349.         }

  350.         private Ref readRef(TreeMap<String, Ref> avail, String path,
  351.                 String name) throws TransportException {
  352.             final String line;
  353.             try (BufferedReader br = openReader(path)) {
  354.                 line = br.readLine();
  355.             } catch (FileNotFoundException noRef) {
  356.                 return null;
  357.             } catch (IOException err) {
  358.                 throw new TransportException(MessageFormat.format(
  359.                         JGitText.get().cannotReadObjectsPath, objectsPath, path,
  360.                         err.getMessage()), err);
  361.             }

  362.             if (line == null) {
  363.                 throw new TransportException(
  364.                         MessageFormat.format(JGitText.get().emptyRef, name));
  365.             }
  366.             if (line.startsWith("ref: ")) { //$NON-NLS-1$
  367.                 final String target = line.substring("ref: ".length()); //$NON-NLS-1$
  368.                 Ref r = avail.get(target);
  369.                 if (r == null)
  370.                     r = readRef(avail, ROOT_DIR + target, target);
  371.                 if (r == null)
  372.                     r = new ObjectIdRef.Unpeeled(Ref.Storage.NEW, target, null);
  373.                 r = new SymbolicRef(name, r);
  374.                 avail.put(r.getName(), r);
  375.                 return r;
  376.             }

  377.             if (ObjectId.isId(line)) {
  378.                 final Ref r = new ObjectIdRef.Unpeeled(loose(avail.get(name)),
  379.                         name, ObjectId.fromString(line));
  380.                 avail.put(r.getName(), r);
  381.                 return r;
  382.             }

  383.             throw new TransportException(
  384.                     MessageFormat.format(JGitText.get().badRef, name, line));
  385.         }

  386.         private Storage loose(Ref r) {
  387.             if (r != null && r.getStorage() == Storage.PACKED) {
  388.                 return Storage.LOOSE_PACKED;
  389.             }
  390.             return Storage.LOOSE;
  391.         }

  392.         @Override
  393.         void close() {
  394.             if (ftp != null) {
  395.                 try {
  396.                     if (ftp.isConnected()) {
  397.                         ftp.disconnect();
  398.                     }
  399.                 } finally {
  400.                     ftp = null;
  401.                 }
  402.             }
  403.         }
  404.     }
  405. }