FileResolver.java

  1. /*
  2.  * Copyright (C) 2009-2022, Google Inc. 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.resolver;

  11. import java.io.File;
  12. import java.io.IOException;
  13. import java.util.Collection;
  14. import java.util.Map;
  15. import java.util.concurrent.ConcurrentHashMap;
  16. import java.util.concurrent.CopyOnWriteArrayList;

  17. import org.eclipse.jgit.errors.RepositoryNotFoundException;
  18. import org.eclipse.jgit.lib.Repository;
  19. import org.eclipse.jgit.lib.RepositoryCache;
  20. import org.eclipse.jgit.lib.RepositoryCache.FileKey;
  21. import org.eclipse.jgit.util.FS;
  22. import org.eclipse.jgit.util.StringUtils;

  23. /**
  24.  * Default resolver serving from the local filesystem.
  25.  *
  26.  * @param <C>
  27.  *            type of connection
  28.  */
  29. public class FileResolver<C> implements RepositoryResolver<C> {
  30.     private volatile boolean exportAll;

  31.     private final Map<String, Repository> exports;

  32.     private final Collection<File> exportBase;

  33.     /**
  34.      * Initialize an empty file based resolver.
  35.      */
  36.     public FileResolver() {
  37.         exports = new ConcurrentHashMap<>();
  38.         exportBase = new CopyOnWriteArrayList<>();
  39.     }

  40.     /**
  41.      * Create a new resolver for the given path.
  42.      *
  43.      * @param basePath
  44.      *            the base path all repositories are rooted under.
  45.      * @param exportAll
  46.      *            if true, exports all repositories, ignoring the check for the
  47.      *            {@code git-daemon-export-ok} files.
  48.      */
  49.     public FileResolver(File basePath, boolean exportAll) {
  50.         this();
  51.         exportDirectory(basePath);
  52.         setExportAll(exportAll);
  53.     }

  54.     /** {@inheritDoc} */
  55.     @Override
  56.     public Repository open(C req, String name)
  57.             throws RepositoryNotFoundException, ServiceNotEnabledException {
  58.         if (isUnreasonableName(name))
  59.             throw new RepositoryNotFoundException(name);

  60.         Repository db = exports.get(StringUtils.nameWithDotGit(name));
  61.         if (db != null) {
  62.             db.incrementOpen();
  63.             return db;
  64.         }

  65.         for (File base : exportBase) {
  66.             File dir = FileKey.resolve(new File(base, name), FS.DETECTED);
  67.             if (dir == null)
  68.                 continue;

  69.             try {
  70.                 FileKey key = FileKey.exact(dir, FS.DETECTED);
  71.                 db = RepositoryCache.open(key, true);
  72.             } catch (IOException e) {
  73.                 throw new RepositoryNotFoundException(name, e);
  74.             }

  75.             try {
  76.                 if (isExportOk(req, name, db)) {
  77.                     // We have to leak the open count to the caller, they
  78.                     // are responsible for closing the repository if we
  79.                     // complete successfully.
  80.                     return db;
  81.                 }
  82.                 throw new ServiceNotEnabledException();

  83.             } catch (RuntimeException | IOException e) {
  84.                 db.close();
  85.                 throw new RepositoryNotFoundException(name, e);

  86.             } catch (ServiceNotEnabledException e) {
  87.                 db.close();
  88.                 throw e;
  89.             }
  90.         }

  91.         if (exportBase.size() == 1) {
  92.             File dir = new File(exportBase.iterator().next(), name);
  93.             throw new RepositoryNotFoundException(name,
  94.                     new RepositoryNotFoundException(dir));
  95.         }

  96.         throw new RepositoryNotFoundException(name);
  97.     }

  98.     /**
  99.      * Whether <code>git-daemon-export-ok</code> is required to export a
  100.      * repository
  101.      *
  102.      * @return false if <code>git-daemon-export-ok</code> is required to export
  103.      *         a repository; true if <code>git-daemon-export-ok</code> is
  104.      *         ignored.
  105.      * @see #setExportAll(boolean)
  106.      */
  107.     public boolean isExportAll() {
  108.         return exportAll;
  109.     }

  110.     /**
  111.      * Set whether or not to export all repositories.
  112.      * <p>
  113.      * If false (the default), repositories must have a
  114.      * <code>git-daemon-export-ok</code> file to be accessed through this
  115.      * daemon.
  116.      * <p>
  117.      * If true, all repositories are available through the daemon, whether or
  118.      * not <code>git-daemon-export-ok</code> exists.
  119.      *
  120.      * @param export a boolean.
  121.      */
  122.     public void setExportAll(boolean export) {
  123.         exportAll = export;
  124.     }

  125.     /**
  126.      * Add a single repository to the set that is exported by this daemon.
  127.      * <p>
  128.      * The existence (or lack-thereof) of <code>git-daemon-export-ok</code> is
  129.      * ignored by this method. The repository is always published.
  130.      *
  131.      * @param name
  132.      *            name the repository will be published under.
  133.      * @param db
  134.      *            the repository instance.
  135.      */
  136.     public void exportRepository(String name, Repository db) {
  137.         exports.put(StringUtils.nameWithDotGit(name), db);
  138.     }

  139.     /**
  140.      * Recursively export all Git repositories within a directory.
  141.      *
  142.      * @param dir
  143.      *            the directory to export. This directory must not itself be a
  144.      *            git repository, but any directory below it which has a file
  145.      *            named <code>git-daemon-export-ok</code> will be published.
  146.      */
  147.     public void exportDirectory(File dir) {
  148.         exportBase.add(dir);
  149.     }

  150.     /**
  151.      * Check if this repository can be served.
  152.      * <p>
  153.      * The default implementation of this method returns true only if either
  154.      * {@link #isExportAll()} is true, or the {@code git-daemon-export-ok} file
  155.      * is present in the repository's directory.
  156.      *
  157.      * @param req
  158.      *            the current HTTP request.
  159.      * @param repositoryName
  160.      *            name of the repository, as present in the URL.
  161.      * @param db
  162.      *            the opened repository instance.
  163.      * @return true if the repository is accessible; false if not.
  164.      * @throws java.io.IOException
  165.      *             the repository could not be accessed, the caller will claim
  166.      *             the repository does not exist.
  167.      */
  168.     protected boolean isExportOk(C req, String repositoryName, Repository db)
  169.             throws IOException {
  170.         if (isExportAll())
  171.             return true;
  172.         else if (db.getDirectory() != null)
  173.             return new File(db.getDirectory(), "git-daemon-export-ok").exists(); //$NON-NLS-1$
  174.         else
  175.             return false;
  176.     }

  177.     private static boolean isUnreasonableName(String name) {
  178.         if (name.length() == 0)
  179.             return true; // no empty paths

  180.         if (name.indexOf('\\') >= 0)
  181.             return true; // no windows/dos style paths
  182.         if (new File(name).isAbsolute())
  183.             return true; // no absolute paths

  184.         if (name.startsWith("../")) //$NON-NLS-1$
  185.             return true; // no "l../etc/passwd"
  186.         if (name.contains("/../")) //$NON-NLS-1$
  187.             return true; // no "foo/../etc/passwd"
  188.         if (name.contains("/./")) //$NON-NLS-1$
  189.             return true; // "foo/./foo" is insane to ask
  190.         if (name.contains("//")) //$NON-NLS-1$
  191.             return true; // double slashes is sloppy, don't use it

  192.         return false; // is a reasonable name
  193.     }
  194. }