LCOV - code coverage report
Current view: top level - httpd/raw - StaticModule.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 42 165 25.5 %
Date: 2022-11-19 15:00:39 Functions: 11 37 29.7 %

          Line data    Source code
       1             : // Copyright (C) 2015 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.httpd.raw;
      16             : 
      17             : import static java.nio.file.Files.exists;
      18             : import static java.nio.file.Files.isReadable;
      19             : 
      20             : import com.google.common.cache.Cache;
      21             : import com.google.common.collect.ImmutableList;
      22             : import com.google.common.flogger.FluentLogger;
      23             : import com.google.gerrit.common.Nullable;
      24             : import com.google.gerrit.extensions.api.GerritApi;
      25             : import com.google.gerrit.httpd.XsrfCookieFilter;
      26             : import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
      27             : import com.google.gerrit.launcher.GerritLauncher;
      28             : import com.google.gerrit.server.cache.CacheModule;
      29             : import com.google.gerrit.server.config.CanonicalWebUrl;
      30             : import com.google.gerrit.server.config.GerritOptions;
      31             : import com.google.gerrit.server.config.GerritServerConfig;
      32             : import com.google.gerrit.server.config.SitePaths;
      33             : import com.google.gerrit.server.experiments.ExperimentFeatures;
      34             : import com.google.inject.Inject;
      35             : import com.google.inject.Key;
      36             : import com.google.inject.Provides;
      37             : import com.google.inject.ProvisionException;
      38             : import com.google.inject.Singleton;
      39             : import com.google.inject.name.Named;
      40             : import com.google.inject.name.Names;
      41             : import com.google.inject.servlet.ServletModule;
      42             : import java.io.File;
      43             : import java.io.FileNotFoundException;
      44             : import java.io.IOException;
      45             : import java.nio.file.FileSystem;
      46             : import java.nio.file.Path;
      47             : import javax.servlet.Filter;
      48             : import javax.servlet.FilterChain;
      49             : import javax.servlet.FilterConfig;
      50             : import javax.servlet.ServletException;
      51             : import javax.servlet.ServletRequest;
      52             : import javax.servlet.ServletResponse;
      53             : import javax.servlet.http.HttpServlet;
      54             : import javax.servlet.http.HttpServletRequest;
      55             : import javax.servlet.http.HttpServletRequestWrapper;
      56             : import javax.servlet.http.HttpServletResponse;
      57             : import org.eclipse.jgit.http.server.GitSmartHttpTools;
      58             : import org.eclipse.jgit.lib.Config;
      59             : 
      60             : public class StaticModule extends ServletModule {
      61          99 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      62             : 
      63             :   public static final String CACHE = "static_content";
      64             : 
      65             :   /**
      66             :    * Paths at which we should serve the main PolyGerrit application {@code index.html}.
      67             :    *
      68             :    * <p>Supports {@code "/*"} as a trailing wildcard.
      69             :    */
      70          99 :   public static final ImmutableList<String> POLYGERRIT_INDEX_PATHS =
      71          99 :       ImmutableList.of(
      72             :           "/",
      73             :           "/c/*",
      74             :           "/id/*",
      75             :           "/p/*",
      76             :           "/q/*",
      77             :           "/x/*",
      78             :           "/admin/*",
      79             :           "/dashboard/*",
      80             :           "/groups/self",
      81             :           "/settings/*",
      82             :           "/topic/*",
      83             :           "/Documentation/q/*");
      84             : 
      85             :   /**
      86             :    * Paths that should be treated as static assets when serving PolyGerrit.
      87             :    *
      88             :    * <p>Supports {@code "/*"} as a trailing wildcard.
      89             :    */
      90          99 :   private static final ImmutableList<String> POLYGERRIT_ASSET_PATHS =
      91          99 :       ImmutableList.of(
      92             :           "/behaviors/*",
      93             :           "/bower_components/*",
      94             :           "/elements/*",
      95             :           "/fonts/*",
      96             :           "/scripts/*",
      97             :           "/styles/*",
      98             :           "/workers/*");
      99             : 
     100             :   private static final String DOC_SERVLET = "DocServlet";
     101             :   private static final String FAVICON_SERVLET = "FaviconServlet";
     102             :   private static final String SERVICE_WORKER_SERVLET = "ServiceWorkerServlet";
     103             :   private static final String POLYGERRIT_INDEX_SERVLET = "PolyGerritUiIndexServlet";
     104             :   private static final String ROBOTS_TXT_SERVLET = "RobotsTxtServlet";
     105             : 
     106             :   private final GerritOptions options;
     107             :   private Paths paths;
     108             : 
     109             :   @Inject
     110          99 :   public StaticModule(GerritOptions options) {
     111          99 :     this.options = options;
     112          99 :   }
     113             : 
     114             :   @Provides
     115             :   @Singleton
     116             :   private Paths getPaths() {
     117          99 :     if (paths == null) {
     118          99 :       paths = new Paths(options);
     119             :     }
     120          99 :     return paths;
     121             :   }
     122             : 
     123             :   @Override
     124             :   protected void configureServlets() {
     125          99 :     serveRegex("^/Documentation$").with(named(DOC_SERVLET));
     126          99 :     serveRegex("^/Documentation/$").with(named(DOC_SERVLET));
     127          99 :     serveRegex("^/Documentation/(.+)$").with(named(DOC_SERVLET));
     128          99 :     serve("/static/*").with(SiteStaticDirectoryServlet.class);
     129          99 :     install(
     130          99 :         new CacheModule() {
     131             :           @Override
     132             :           protected void configure() {
     133          99 :             cache(CACHE, Path.class, Resource.class)
     134          99 :                 .maximumWeight(1 << 20)
     135          99 :                 .weigher(ResourceServlet.Weigher.class);
     136          99 :           }
     137             :         });
     138          99 :     if (!options.headless()) {
     139           0 :       install(new CoreStaticModule());
     140           0 :       install(new PolyGerritModule());
     141             :     }
     142          99 :   }
     143             : 
     144             :   @Provides
     145             :   @Singleton
     146             :   @Named(DOC_SERVLET)
     147             :   HttpServlet getDocServlet(
     148             :       @Named(CACHE) Cache<Path, Resource> cache, ExperimentFeatures experimentFeatures) {
     149          99 :     Paths p = getPaths();
     150          99 :     if (p.warFs != null) {
     151           0 :       return new WarDocServlet(cache, p.warFs, experimentFeatures);
     152          99 :     } else if (p.unpackedWar != null && !p.isDev()) {
     153          99 :       return new DirectoryDocServlet(cache, p.unpackedWar, experimentFeatures);
     154             :     } else {
     155           0 :       return new HttpServlet() {
     156             :         private static final long serialVersionUID = 1L;
     157             : 
     158             :         @Override
     159             :         protected void service(HttpServletRequest req, HttpServletResponse resp)
     160             :             throws IOException {
     161           0 :           resp.sendError(HttpServletResponse.SC_NOT_FOUND);
     162           0 :         }
     163             :       };
     164             :     }
     165             :   }
     166             : 
     167           0 :   private class CoreStaticModule extends ServletModule {
     168             :     @Override
     169             :     public void configureServlets() {
     170           0 :       serve("/robots.txt").with(named(ROBOTS_TXT_SERVLET));
     171           0 :       serve("/favicon.ico").with(named(FAVICON_SERVLET));
     172           0 :       serve("/service-worker.js").with(named(SERVICE_WORKER_SERVLET));
     173           0 :     }
     174             : 
     175             :     @Provides
     176             :     @Singleton
     177             :     @Named(ROBOTS_TXT_SERVLET)
     178             :     HttpServlet getRobotsTxtServlet(
     179             :         @GerritServerConfig Config cfg,
     180             :         SitePaths sitePaths,
     181             :         @Named(CACHE) Cache<Path, Resource> cache) {
     182           0 :       Path configPath = sitePaths.resolve(cfg.getString("httpd", null, "robotsFile"));
     183           0 :       if (configPath != null) {
     184           0 :         if (exists(configPath) && isReadable(configPath)) {
     185           0 :           return new SingleFileServlet(cache, configPath, true);
     186             :         }
     187           0 :         logger.atWarning().log("Cannot read httpd.robotsFile, using default");
     188             :       }
     189           0 :       Paths p = getPaths();
     190           0 :       if (p.warFs != null) {
     191           0 :         return new SingleFileServlet(cache, p.warFs.getPath("/robots.txt"), false);
     192             :       }
     193           0 :       return new SingleFileServlet(cache, webappSourcePath("robots.txt"), true);
     194             :     }
     195             : 
     196             :     @Provides
     197             :     @Singleton
     198             :     @Named(FAVICON_SERVLET)
     199             :     HttpServlet getFaviconServlet(@Named(CACHE) Cache<Path, Resource> cache) {
     200           0 :       Paths p = getPaths();
     201           0 :       if (p.warFs != null) {
     202           0 :         return new SingleFileServlet(cache, p.warFs.getPath("/favicon.ico"), false);
     203             :       }
     204           0 :       return new SingleFileServlet(cache, webappSourcePath("favicon.ico"), true);
     205             :     }
     206             : 
     207             :     @Provides
     208             :     @Singleton
     209             :     @Named(SERVICE_WORKER_SERVLET)
     210             :     HttpServlet getServiceWorkerServlet(@Named(CACHE) Cache<Path, Resource> cache) {
     211           0 :       Paths p = getPaths();
     212           0 :       if (p.warFs != null) {
     213           0 :         return new SingleFileServlet(
     214           0 :             cache, p.warFs.getPath("/polygerrit_ui/workers/service-worker.js"), false);
     215             :       }
     216           0 :       return new SingleFileServlet(
     217           0 :           cache, webappSourcePath("polygerrit_ui/workers/service-worker.js"), true);
     218             :     }
     219             : 
     220             :     private Path webappSourcePath(String name) {
     221           0 :       Paths p = getPaths();
     222           0 :       if (p.unpackedWar != null) {
     223           0 :         return p.unpackedWar.resolve(name);
     224             :       }
     225           0 :       return p.sourceRoot.resolve("webapp/" + name);
     226             :     }
     227             :   }
     228             : 
     229           0 :   private class PolyGerritModule extends ServletModule {
     230             :     @Override
     231             :     public void configureServlets() {
     232           0 :       for (String p : POLYGERRIT_INDEX_PATHS) {
     233           0 :         filter(p).through(XsrfCookieFilter.class);
     234           0 :       }
     235           0 :       filter("/*").through(PolyGerritFilter.class);
     236           0 :     }
     237             : 
     238             :     @Provides
     239             :     @Singleton
     240             :     @Named(POLYGERRIT_INDEX_SERVLET)
     241             :     HttpServlet getPolyGerritUiIndexServlet(
     242             :         @CanonicalWebUrl @Nullable String canonicalUrl,
     243             :         @GerritServerConfig Config cfg,
     244             :         GerritApi gerritApi,
     245             :         ExperimentFeatures experimentFeatures) {
     246           0 :       String cdnPath = options.devCdn().orElse(cfg.getString("gerrit", null, "cdnPath"));
     247           0 :       String faviconPath = cfg.getString("gerrit", null, "faviconPath");
     248           0 :       return new IndexServlet(canonicalUrl, cdnPath, faviconPath, gerritApi, experimentFeatures);
     249             :     }
     250             : 
     251             :     @Provides
     252             :     @Singleton
     253             :     PolyGerritUiServlet getPolyGerritUiServlet(@Named(CACHE) Cache<Path, Resource> cache) {
     254           0 :       return new PolyGerritUiServlet(cache, polyGerritBasePath());
     255             :     }
     256             : 
     257             :     private Path polyGerritBasePath() {
     258           0 :       Paths p = getPaths();
     259             : 
     260           0 :       return p.warFs != null
     261           0 :           ? p.warFs.getPath("/polygerrit_ui")
     262           0 :           : p.unpackedWar.resolve("polygerrit_ui");
     263             :     }
     264             :   }
     265             : 
     266             :   private static class Paths {
     267             :     private final FileSystem warFs;
     268             :     private final Path sourceRoot;
     269             :     private final Path unpackedWar;
     270             :     private final boolean development;
     271             : 
     272          99 :     private Paths(GerritOptions options) {
     273             :       try {
     274          99 :         File launcherLoadedFrom = getLauncherLoadedFrom();
     275          99 :         if (launcherLoadedFrom != null && launcherLoadedFrom.getName().endsWith(".jar")) {
     276             :           // Special case: unpacked war archive deployed in container.
     277             :           // The path is something like:
     278             :           // <container>/<gerrit>/WEB-INF/lib/launcher.jar
     279             :           // Switch to exploded war case with <container>/webapp>/<gerrit>
     280             :           // root directory
     281          99 :           warFs = null;
     282          99 :           unpackedWar =
     283          99 :               java.nio.file.Paths.get(
     284          99 :                   launcherLoadedFrom.getParentFile().getParentFile().getParentFile().toURI());
     285          99 :           sourceRoot = null;
     286          99 :           development = false;
     287          99 :           return;
     288             :         }
     289           0 :         warFs = getDistributionArchive(launcherLoadedFrom);
     290           0 :         if (warFs == null) {
     291           0 :           unpackedWar = makeWarTempDir();
     292           0 :           development = true;
     293           0 :         } else if (options.devCdn().isPresent()) {
     294           0 :           unpackedWar = null;
     295           0 :           development = true;
     296             :         } else {
     297           0 :           unpackedWar = null;
     298           0 :           development = false;
     299           0 :           sourceRoot = null;
     300           0 :           return;
     301             :         }
     302           0 :       } catch (IOException e) {
     303           0 :         throw new ProvisionException("Error initializing static content paths", e);
     304           0 :       }
     305             : 
     306           0 :       sourceRoot = getSourceRootOrNull();
     307           0 :     }
     308             : 
     309             :     @Nullable
     310             :     private static Path getSourceRootOrNull() {
     311             :       try {
     312           0 :         return GerritLauncher.resolveInSourceRoot(".");
     313           0 :       } catch (FileNotFoundException e) {
     314           0 :         return null;
     315             :       }
     316             :     }
     317             : 
     318             :     @Nullable
     319             :     private FileSystem getDistributionArchive(File war) throws IOException {
     320           0 :       if (war == null) {
     321           0 :         return null;
     322             :       }
     323           0 :       return GerritLauncher.getZipFileSystem(war.toPath());
     324             :     }
     325             : 
     326             :     @Nullable
     327             :     private File getLauncherLoadedFrom() {
     328             :       File war;
     329             :       try {
     330          99 :         war = GerritLauncher.getDistributionArchive();
     331           0 :       } catch (IOException e) {
     332           0 :         if ((e instanceof FileNotFoundException)
     333           0 :             && GerritLauncher.NOT_ARCHIVED.equals(e.getMessage())) {
     334           0 :           return null;
     335             :         }
     336           0 :         throw new ProvisionException("Error reading gerrit.war", e);
     337          99 :       }
     338          99 :       return war;
     339             :     }
     340             : 
     341             :     private boolean isDev() {
     342          99 :       return development;
     343             :     }
     344             : 
     345             :     private Path makeWarTempDir() {
     346             :       // Obtain our local temporary directory, but it comes back as a file
     347             :       // so we have to switch it to be a directory post creation.
     348             :       //
     349             :       try {
     350           0 :         File dstwar = GerritLauncher.createTempFile("gerrit_", "war");
     351           0 :         if (!dstwar.delete() || !dstwar.mkdir()) {
     352           0 :           throw new IOException("Cannot mkdir " + dstwar.getAbsolutePath());
     353             :         }
     354             : 
     355             :         // Jetty normally refuses to serve out of a symlinked directory, as
     356             :         // a security feature. Try to resolve out any symlinks in the path.
     357             :         //
     358             :         try {
     359           0 :           return dstwar.getCanonicalFile().toPath();
     360           0 :         } catch (IOException e) {
     361           0 :           return dstwar.getAbsoluteFile().toPath();
     362             :         }
     363           0 :       } catch (IOException e) {
     364           0 :         throw new ProvisionException("Cannot create war tempdir", e);
     365             :       }
     366             :     }
     367             :   }
     368             : 
     369             :   private static Key<HttpServlet> named(String name) {
     370          99 :     return Key.get(HttpServlet.class, Names.named(name));
     371             :   }
     372             : 
     373             :   @Singleton
     374             :   private static class PolyGerritFilter implements Filter {
     375             :     private final HttpServlet polyGerritIndex;
     376             :     private final PolyGerritUiServlet polygerritUI;
     377             : 
     378             :     @Inject
     379             :     PolyGerritFilter(
     380             :         @Named(POLYGERRIT_INDEX_SERVLET) HttpServlet polyGerritIndex,
     381           0 :         PolyGerritUiServlet polygerritUI) {
     382           0 :       this.polyGerritIndex = polyGerritIndex;
     383           0 :       this.polygerritUI = polygerritUI;
     384           0 :     }
     385             : 
     386             :     @Override
     387           0 :     public void init(FilterConfig filterConfig) throws ServletException {}
     388             : 
     389             :     @Override
     390           0 :     public void destroy() {}
     391             : 
     392             :     @Override
     393             :     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
     394             :         throws IOException, ServletException {
     395           0 :       HttpServletRequest req = (HttpServletRequest) request;
     396           0 :       HttpServletResponse res = (HttpServletResponse) response;
     397             : 
     398           0 :       if (!GitSmartHttpTools.isGitClient(req)) {
     399           0 :         GuiceFilterRequestWrapper reqWrapper = new GuiceFilterRequestWrapper(req);
     400           0 :         String path = pathInfo(req);
     401             : 
     402           0 :         if (isPolyGerritIndex(path)) {
     403           0 :           polyGerritIndex.service(reqWrapper, res);
     404           0 :           return;
     405             :         }
     406           0 :         if (isPolyGerritAsset(path)) {
     407           0 :           polygerritUI.service(reqWrapper, res);
     408           0 :           return;
     409             :         }
     410             :       }
     411             : 
     412           0 :       chain.doFilter(req, res);
     413           0 :     }
     414             : 
     415             :     private static String pathInfo(HttpServletRequest req) {
     416           0 :       String uri = req.getRequestURI();
     417           0 :       String ctx = req.getContextPath();
     418           0 :       return uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri;
     419             :     }
     420             : 
     421             :     private static boolean isPolyGerritAsset(String path) {
     422           0 :       return matchPath(POLYGERRIT_ASSET_PATHS, path);
     423             :     }
     424             : 
     425             :     private static boolean isPolyGerritIndex(String path) {
     426           0 :       return matchPath(POLYGERRIT_INDEX_PATHS, path);
     427             :     }
     428             : 
     429             :     private static boolean matchPath(Iterable<String> paths, String path) {
     430           0 :       for (String p : paths) {
     431           0 :         if (p.endsWith("/*")) {
     432           0 :           if (path.regionMatches(0, p, 0, p.length() - 1)) {
     433           0 :             return true;
     434             :           }
     435           0 :         } else if (p.equals(path)) {
     436           0 :           return true;
     437             :         }
     438           0 :       }
     439           0 :       return false;
     440             :     }
     441             :   }
     442             : 
     443             :   private static class GuiceFilterRequestWrapper extends HttpServletRequestWrapper {
     444             :     GuiceFilterRequestWrapper(HttpServletRequest req) {
     445           0 :       super(req);
     446           0 :     }
     447             : 
     448             :     @Nullable
     449             :     @Override
     450             :     public String getPathInfo() {
     451           0 :       String uri = getRequestURI();
     452           0 :       String ctx = getContextPath();
     453             :       // This is a workaround for long standing guice filter bug:
     454             :       // https://github.com/google/guice/issues/807
     455           0 :       String res = uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri;
     456             : 
     457             :       // Match the logic in the ResourceServlet, that re-add "/"
     458             :       // for null path info
     459           0 :       if ("/".equals(res)) {
     460           0 :         return null;
     461             :       }
     462           0 :       return res;
     463             :     }
     464             :   }
     465             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750