LCOV - code coverage report
Current view: top level - httpd/gitweb - GitwebServlet.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 0 396 0.0 %
Date: 2022-11-19 15:00:39 Functions: 0 28 0.0 %

          Line data    Source code
       1             : // Copyright (C) 2009 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             : // CGI environment and execution management portions are:
      16             : //
      17             : // ========================================================================
      18             : // Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd.
      19             : // ------------------------------------------------------------------------
      20             : // All rights reserved. This program and the accompanying materials
      21             : // are made available under the terms of the Eclipse Public License v1.0
      22             : // and Apache License v2.0 which accompanies this distribution.
      23             : // The Eclipse Public License is available at
      24             : // http://www.eclipse.org/legal/epl-v10.html
      25             : // The Apache License v2.0 is available at
      26             : // http://www.opensource.org/licenses/apache2.0.php
      27             : // You may elect to redistribute this code under either of these licenses.
      28             : // ========================================================================
      29             : 
      30             : package com.google.gerrit.httpd.gitweb;
      31             : 
      32             : import static java.nio.charset.StandardCharsets.ISO_8859_1;
      33             : import static java.nio.charset.StandardCharsets.UTF_8;
      34             : 
      35             : import com.google.common.base.CharMatcher;
      36             : import com.google.common.base.Splitter;
      37             : import com.google.common.flogger.FluentLogger;
      38             : import com.google.gerrit.common.PageLinks;
      39             : import com.google.gerrit.entities.Project;
      40             : import com.google.gerrit.extensions.restapi.AuthException;
      41             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      42             : import com.google.gerrit.extensions.restapi.Url;
      43             : import com.google.gerrit.server.AnonymousUser;
      44             : import com.google.gerrit.server.CurrentUser;
      45             : import com.google.gerrit.server.IdentifiedUser;
      46             : import com.google.gerrit.server.config.AllProjectsName;
      47             : import com.google.gerrit.server.config.GerritServerConfig;
      48             : import com.google.gerrit.server.config.GitwebCgiConfig;
      49             : import com.google.gerrit.server.config.GitwebConfig;
      50             : import com.google.gerrit.server.config.SitePaths;
      51             : import com.google.gerrit.server.git.DelegateRepository;
      52             : import com.google.gerrit.server.git.GitRepositoryManager;
      53             : import com.google.gerrit.server.permissions.PermissionBackend;
      54             : import com.google.gerrit.server.permissions.PermissionBackendException;
      55             : import com.google.gerrit.server.permissions.ProjectPermission;
      56             : import com.google.gerrit.server.project.ProjectCache;
      57             : import com.google.gerrit.server.project.ProjectState;
      58             : import com.google.gerrit.server.ssh.SshInfo;
      59             : import com.google.gerrit.util.http.CacheHeaders;
      60             : import com.google.inject.Inject;
      61             : import com.google.inject.Provider;
      62             : import com.google.inject.ProvisionException;
      63             : import com.google.inject.Singleton;
      64             : import java.io.BufferedInputStream;
      65             : import java.io.BufferedReader;
      66             : import java.io.EOFException;
      67             : import java.io.File;
      68             : import java.io.IOException;
      69             : import java.io.InputStream;
      70             : import java.io.InputStreamReader;
      71             : import java.io.OutputStream;
      72             : import java.io.PrintWriter;
      73             : import java.net.URI;
      74             : import java.net.URISyntaxException;
      75             : import java.nio.file.Files;
      76             : import java.nio.file.Path;
      77             : import java.util.Collections;
      78             : import java.util.HashMap;
      79             : import java.util.HashSet;
      80             : import java.util.List;
      81             : import java.util.Map;
      82             : import java.util.Optional;
      83             : import java.util.Set;
      84             : import java.util.stream.Collectors;
      85             : import javax.servlet.http.HttpServlet;
      86             : import javax.servlet.http.HttpServletRequest;
      87             : import javax.servlet.http.HttpServletResponse;
      88             : import org.eclipse.jgit.errors.RepositoryNotFoundException;
      89             : import org.eclipse.jgit.internal.storage.file.FileRepository;
      90             : import org.eclipse.jgit.lib.Config;
      91             : import org.eclipse.jgit.lib.Repository;
      92             : 
      93             : /** Invokes {@code gitweb.cgi} for the project given in {@code p}. */
      94             : @Singleton
      95             : class GitwebServlet extends HttpServlet {
      96             :   private static final long serialVersionUID = 1L;
      97             : 
      98           0 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      99             : 
     100             :   private static final String PROJECT_LIST_ACTION = "project_list";
     101             :   private static final int BUFFER_SIZE = 8192;
     102             : 
     103             :   private final Set<String> deniedActions;
     104             :   private final Path gitwebCgi;
     105             :   private final URI gitwebUrl;
     106             :   private final GitRepositoryManager repoManager;
     107             :   private final ProjectCache projectCache;
     108             :   private final PermissionBackend permissionBackend;
     109             :   private final Provider<AnonymousUser> anonymousUserProvider;
     110             :   private final Provider<CurrentUser> userProvider;
     111             :   private final EnvList _env;
     112             : 
     113             :   @SuppressWarnings("CheckReturnValue")
     114             :   @Inject
     115             :   GitwebServlet(
     116             :       GitRepositoryManager repoManager,
     117             :       ProjectCache projectCache,
     118             :       PermissionBackend permissionBackend,
     119             :       Provider<CurrentUser> userProvider,
     120             :       SitePaths site,
     121             :       @GerritServerConfig Config cfg,
     122             :       SshInfo sshInfo,
     123             :       Provider<AnonymousUser> anonymousUserProvider,
     124             :       GitwebConfig gitwebConfig,
     125             :       GitwebCgiConfig gitwebCgiConfig,
     126             :       AllProjectsName allProjects)
     127           0 :       throws IOException {
     128           0 :     this.repoManager = repoManager;
     129           0 :     this.projectCache = projectCache;
     130           0 :     this.permissionBackend = permissionBackend;
     131           0 :     this.anonymousUserProvider = anonymousUserProvider;
     132           0 :     this.userProvider = userProvider;
     133           0 :     this.gitwebCgi = gitwebCgiConfig.getGitwebCgi();
     134           0 :     this.deniedActions = new HashSet<>();
     135             : 
     136             :     // ensure that Gitweb works on supported repository type by checking All-Projects project
     137           0 :     getProjectRoot(allProjects);
     138             : 
     139           0 :     final String url = gitwebConfig.getUrl();
     140           0 :     if (url != null && !url.equals("gitweb")) {
     141           0 :       URI uri = null;
     142             :       try {
     143           0 :         uri = new URI(url);
     144           0 :       } catch (URISyntaxException e) {
     145           0 :         logger.atSevere().log("Invalid gitweb.url: %s", url);
     146           0 :       }
     147           0 :       gitwebUrl = uri;
     148           0 :     } else {
     149           0 :       gitwebUrl = null;
     150             :     }
     151             : 
     152           0 :     deniedActions.add("forks");
     153           0 :     deniedActions.add("opml");
     154           0 :     deniedActions.add("project_index");
     155             : 
     156           0 :     _env = new EnvList();
     157           0 :     makeSiteConfig(site, cfg, sshInfo);
     158             : 
     159           0 :     if (!_env.envMap.containsKey("SystemRoot")) {
     160           0 :       String os = System.getProperty("os.name");
     161           0 :       if (os != null && os.toLowerCase().contains("windows")) {
     162           0 :         String sysroot = System.getenv("SystemRoot");
     163           0 :         if (sysroot == null || sysroot.isEmpty()) {
     164           0 :           sysroot = "C:\\WINDOWS";
     165             :         }
     166           0 :         _env.set("SystemRoot", sysroot);
     167             :       }
     168             :     }
     169             : 
     170           0 :     if (!_env.envMap.containsKey("PATH")) {
     171           0 :       _env.set("PATH", System.getenv("PATH"));
     172             :     }
     173           0 :   }
     174             : 
     175             :   private void makeSiteConfig(SitePaths site, Config cfg, SshInfo sshInfo) throws IOException {
     176           0 :     if (!Files.exists(site.tmp_dir)) {
     177           0 :       Files.createDirectories(site.tmp_dir);
     178             :     }
     179           0 :     Path myconf = Files.createTempFile(site.tmp_dir, "gitweb_config", ".perl");
     180             : 
     181             :     // To make our configuration file only readable or writable by us; this reduces the chances of
     182             :     // someone tampering with the file.
     183           0 :     File myconfFile = myconf.toFile();
     184           0 :     myconfFile.setWritable(false, false /* all */);
     185           0 :     myconfFile.setReadable(false, false /* all */);
     186           0 :     myconfFile.setExecutable(false, false /* all */);
     187             : 
     188           0 :     myconfFile.setWritable(true, true /* owner only */);
     189           0 :     myconfFile.setReadable(true, true /* owner only */);
     190             : 
     191           0 :     myconfFile.deleteOnExit();
     192             : 
     193           0 :     _env.set("GIT_DIR", ".");
     194           0 :     _env.set("GITWEB_CONFIG", myconf.toAbsolutePath().toString());
     195             : 
     196           0 :     try (PrintWriter p = new PrintWriter(Files.newBufferedWriter(myconf, UTF_8))) {
     197           0 :       p.print("# Autogenerated by Gerrit Code Review \n");
     198           0 :       p.print("# DO NOT EDIT\n");
     199           0 :       p.print("\n");
     200             : 
     201             :       // We are mounted at the same level in the context as the main
     202             :       // UI, so we can include the same header and footer scheme.
     203             :       //
     204           0 :       Path hdr = site.site_header;
     205           0 :       if (Files.isRegularFile(hdr)) {
     206           0 :         p.print("$site_header = " + quoteForPerl(hdr) + ";\n");
     207             :       }
     208           0 :       Path ftr = site.site_footer;
     209           0 :       if (Files.isRegularFile(ftr)) {
     210           0 :         p.print("$site_footer = " + quoteForPerl(ftr) + ";\n");
     211             :       }
     212             : 
     213             :       // Top level should return to Gerrit's UI.
     214             :       //
     215           0 :       p.print("$home_link = $ENV{'GERRIT_CONTEXT_PATH'};\n");
     216           0 :       p.print("$home_link_str = 'Code Review';\n");
     217             : 
     218           0 :       p.print("$favicon = 'favicon.ico';\n");
     219           0 :       p.print("$logo = 'gitweb-logo.png';\n");
     220           0 :       p.print("$javascript = 'gitweb.js';\n");
     221           0 :       p.print("@stylesheets = ('gitweb-default.css');\n");
     222           0 :       Path css = site.site_css;
     223           0 :       if (Files.isRegularFile(css)) {
     224           0 :         p.print("push @stylesheets, 'gitweb-site.css';\n");
     225             :       }
     226             : 
     227             :       // Try to make the title match Gerrit's normal window title
     228             :       // scheme of host followed by 'Code Review'.
     229             :       //
     230           0 :       p.print("$site_name = $home_link_str;\n");
     231           0 :       p.print("$site_name = qq{$1 $site_name} if ");
     232           0 :       p.print("$ENV{'SERVER_NAME'} =~ m,^([^.]+(?:\\.[^.]+)?)(?:\\.|$),;\n");
     233             : 
     234             :       // Assume by default that XSS is a problem, and try to prevent it.
     235             :       //
     236           0 :       p.print("$prevent_xss = 1;\n");
     237             : 
     238             :       // Generate URLs using smart http://
     239             :       //
     240           0 :       p.print("{\n");
     241           0 :       p.print("  my $secure = $ENV{'HTTPS'} =~ /^ON$/i;\n");
     242           0 :       p.print("  my $http_url = $secure ? 'https://' : 'http://';\n");
     243           0 :       p.print("  $http_url .= qq{$ENV{'GERRIT_USER_NAME'}@}\n");
     244           0 :       p.print("    unless $ENV{'GERRIT_ANONYMOUS_READ'};\n");
     245           0 :       p.print("  $http_url .= $ENV{'SERVER_NAME'};\n");
     246           0 :       p.print("  $http_url .= qq{:$ENV{'SERVER_PORT'}}\n");
     247           0 :       p.print("    if (( $secure && $ENV{'SERVER_PORT'} != 443)\n");
     248           0 :       p.print("     || (!$secure && $ENV{'SERVER_PORT'} != 80)\n");
     249           0 :       p.print("    );\n");
     250           0 :       p.print("  my $context = $ENV{'GERRIT_CONTEXT_PATH'};\n");
     251           0 :       p.print("  chop($context);\n");
     252           0 :       p.print("  $http_url .= qq{$context};\n");
     253           0 :       p.print("  $http_url .= qq{/a}\n");
     254           0 :       p.print("    unless $ENV{'GERRIT_ANONYMOUS_READ'};\n");
     255           0 :       p.print("  push @git_base_url_list, $http_url;\n");
     256           0 :       p.print("}\n");
     257             : 
     258             :       // Generate URLs using anonymous git://
     259             :       //
     260           0 :       String url = cfg.getString("gerrit", null, "canonicalGitUrl");
     261           0 :       if (url != null) {
     262           0 :         if (url.endsWith("/")) {
     263           0 :           url = url.substring(0, url.length() - 1);
     264             :         }
     265           0 :         p.print("if ($ENV{'GERRIT_ANONYMOUS_READ'}) {\n");
     266           0 :         p.print("  push @git_base_url_list, ");
     267           0 :         p.print(quoteForPerl(url));
     268           0 :         p.print(";\n");
     269           0 :         p.print("}\n");
     270             :       }
     271             : 
     272             :       // Generate URLs using authenticated ssh://
     273             :       //
     274           0 :       if (sshInfo != null && !sshInfo.getHostKeys().isEmpty()) {
     275           0 :         String sshAddr = sshInfo.getHostKeys().get(0).getHost();
     276           0 :         p.print("if ($ENV{'GERRIT_USER_NAME'}) {\n");
     277           0 :         p.print("  push @git_base_url_list, join('', 'ssh://'");
     278           0 :         p.print(", $ENV{'GERRIT_USER_NAME'}");
     279           0 :         p.print(", '@'");
     280           0 :         if (sshAddr.startsWith("*:") || "".equals(sshAddr)) {
     281           0 :           p.print(", $ENV{'SERVER_NAME'}");
     282             :         }
     283           0 :         if (sshAddr.startsWith("*")) {
     284           0 :           sshAddr = sshAddr.substring(1);
     285             :         }
     286           0 :         p.print(", " + quoteForPerl(sshAddr));
     287           0 :         p.print(");\n");
     288           0 :         p.print("}\n");
     289             :       }
     290             : 
     291             :       // Link back to Gerrit (when possible, to matching review record).
     292             :       // Supported gitweb's hash values are:
     293             :       // - (missing),
     294             :       // - HEAD,
     295             :       // - refs/heads/<branch>,
     296             :       // - refs/changes/*/<change>/*,
     297             :       // - <revision>.
     298             :       //
     299           0 :       p.print("sub add_review_link {\n");
     300           0 :       p.print("  my $h = shift;\n");
     301           0 :       p.print("  my $q;\n");
     302           0 :       p.print("  if (!$h || $h eq 'HEAD') {\n");
     303           0 :       p.print("    $q = qq{#/q/project:$ENV{'GERRIT_PROJECT_NAME'}};\n");
     304           0 :       p.print("  } elsif ($h =~ /^refs\\/heads\\/([-\\w]+)$/) {\n");
     305           0 :       p.print("    $q = qq{#/q/project:$ENV{'GERRIT_PROJECT_NAME'}");
     306           0 :       p.print("+branch:$1};\n"); // wrapped
     307           0 :       p.print("  } elsif ($h =~ /^refs\\/changes\\/\\d{2}\\/(\\d+)\\/\\d+$/) ");
     308           0 :       p.print("{\n"); // wrapped
     309           0 :       p.print("    $q = qq{#/c/$1};\n");
     310           0 :       p.print("  } else {\n");
     311           0 :       p.print("    $q = qq{#/q/$h};\n");
     312           0 :       p.print("  }\n");
     313           0 :       p.print("  my $r = qq{$ENV{'GERRIT_CONTEXT_PATH'}$q};\n");
     314           0 :       p.print("  push @{$feature{'actions'}{'default'}},\n");
     315           0 :       p.print("      ('review',$r,'commitdiff');\n");
     316           0 :       p.print("}\n");
     317           0 :       p.print("if ($cgi->param('hb')) {\n");
     318           0 :       p.print("  add_review_link(scalar $cgi->param('hb'));\n");
     319           0 :       p.print("} elsif ($cgi->param('h')) {\n");
     320           0 :       p.print("  add_review_link(scalar $cgi->param('h'));\n");
     321           0 :       p.print("} else {\n");
     322           0 :       p.print("  add_review_link();\n");
     323           0 :       p.print("}\n");
     324             : 
     325             :       // If the administrator has created a site-specific gitweb_config,
     326             :       // load that before we perform any final overrides.
     327             :       //
     328           0 :       Path sitecfg = site.site_gitweb;
     329           0 :       if (Files.isRegularFile(sitecfg)) {
     330           0 :         p.print("$GITWEB_CONFIG = " + quoteForPerl(sitecfg) + ";\n");
     331           0 :         p.print("if (-e $GITWEB_CONFIG) {\n");
     332           0 :         p.print("  do " + quoteForPerl(sitecfg) + ";\n");
     333           0 :         p.print("}\n");
     334             :       }
     335             : 
     336           0 :       p.print("$projectroot = $ENV{'GITWEB_PROJECTROOT'};\n");
     337             : 
     338             :       // Permit exporting only the project we were started for.
     339             :       // We use the name under $projectroot in case symlinks
     340             :       // were involved in the path.
     341             :       //
     342           0 :       p.print("$export_auth_hook = sub {\n");
     343           0 :       p.print("    my $dir = shift;\n");
     344           0 :       p.print("    my $name = $ENV{'GERRIT_PROJECT_NAME'};\n");
     345           0 :       p.print("    my $allow = qq{$projectroot/$name.git};\n");
     346           0 :       p.print("    return $dir eq $allow;\n");
     347           0 :       p.print("  };\n");
     348             : 
     349             :       // Do not allow the administrator to enable path info, its
     350             :       // not a URL format we currently support.
     351             :       //
     352           0 :       p.print("$feature{'pathinfo'}{'override'} = 0;\n");
     353           0 :       p.print("$feature{'pathinfo'}{'default'} = [0];\n");
     354             : 
     355             :       // We don't do forking, so don't allow it to be enabled.
     356             :       //
     357           0 :       p.print("$feature{'forks'}{'override'} = 0;\n");
     358           0 :       p.print("$feature{'forks'}{'default'} = [0];\n");
     359             :     }
     360             : 
     361           0 :     myconfFile.setReadOnly();
     362           0 :   }
     363             : 
     364             :   private static String quoteForPerl(Path value) {
     365           0 :     return quoteForPerl(value.toAbsolutePath().toString());
     366             :   }
     367             : 
     368             :   private static String quoteForPerl(String value) {
     369           0 :     if (value == null || value.isEmpty()) {
     370           0 :       return "''";
     371             :     }
     372           0 :     if (!value.contains("'")) {
     373           0 :       return "'" + value + "'";
     374             :     }
     375           0 :     if (!value.contains("{") && !value.contains("}")) {
     376           0 :       return "q{" + value + "}";
     377             :     }
     378           0 :     throw new IllegalArgumentException("Cannot quote in Perl: " + value);
     379             :   }
     380             : 
     381             :   @Override
     382             :   protected void service(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
     383           0 :     if (req.getQueryString() == null || req.getQueryString().isEmpty()) {
     384             :       // No query string? They want the project list, which we don't
     385             :       // currently support. Return to Gerrit's own web UI.
     386             :       //
     387           0 :       rsp.sendRedirect(req.getContextPath() + "/");
     388           0 :       return;
     389             :     }
     390             : 
     391           0 :     final Map<String, String> params = getParameters(req);
     392           0 :     String a = params.get("a");
     393           0 :     if (a != null) {
     394           0 :       if (deniedActions.contains(a)) {
     395           0 :         rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
     396           0 :         return;
     397             :       }
     398             : 
     399           0 :       if (a.equals(PROJECT_LIST_ACTION)) {
     400           0 :         rsp.sendRedirect(
     401           0 :             req.getContextPath()
     402             :                 + "/#"
     403             :                 + PageLinks.ADMIN_PROJECTS
     404             :                 + "?filter="
     405           0 :                 + Url.encode(params.get("pf") + "/"));
     406           0 :         return;
     407             :       }
     408             :     }
     409             : 
     410           0 :     String name = params.get("p");
     411           0 :     if (name == null) {
     412           0 :       rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
     413           0 :       return;
     414             :     }
     415           0 :     if (name.endsWith(".git")) {
     416           0 :       name = name.substring(0, name.length() - 4);
     417             :     }
     418             : 
     419           0 :     Project.NameKey nameKey = Project.nameKey(name);
     420             :     Optional<ProjectState> projectState;
     421             :     try {
     422           0 :       projectState = projectCache.get(nameKey);
     423           0 :       if (!projectState.isPresent()) {
     424           0 :         sendErrorOrRedirect(req, rsp, HttpServletResponse.SC_NOT_FOUND);
     425           0 :         return;
     426             :       }
     427             : 
     428           0 :       projectState.get().checkStatePermitsRead();
     429           0 :       permissionBackend.user(userProvider.get()).project(nameKey).check(ProjectPermission.READ);
     430           0 :     } catch (AuthException e) {
     431           0 :       sendErrorOrRedirect(req, rsp, HttpServletResponse.SC_NOT_FOUND);
     432           0 :       return;
     433           0 :     } catch (IOException | PermissionBackendException err) {
     434           0 :       logger.atSevere().withCause(err).log("cannot load %s", name);
     435           0 :       rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
     436           0 :       return;
     437           0 :     } catch (ResourceConflictException e) {
     438           0 :       sendErrorOrRedirect(req, rsp, HttpServletResponse.SC_CONFLICT);
     439           0 :       return;
     440           0 :     }
     441             : 
     442           0 :     try (Repository repo = repoManager.openRepository(nameKey)) {
     443           0 :       CacheHeaders.setNotCacheable(rsp);
     444           0 :       exec(req, rsp, projectState.get());
     445           0 :     } catch (RepositoryNotFoundException e) {
     446           0 :       getServletContext().log("Cannot open repository", e);
     447           0 :       rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
     448           0 :     }
     449           0 :   }
     450             : 
     451             :   /**
     452             :    * Sends error response if the user is authenticated. Or redirect the user to the login page. By
     453             :    * doing this, anonymous users cannot infer the existence of a resource from the status code.
     454             :    */
     455             :   private void sendErrorOrRedirect(HttpServletRequest req, HttpServletResponse rsp, int statusCode)
     456             :       throws IOException {
     457           0 :     if (userProvider.get().isIdentifiedUser()) {
     458           0 :       rsp.sendError(statusCode);
     459             :     } else {
     460           0 :       rsp.sendRedirect(getLoginRedirectUrl(req));
     461             :     }
     462           0 :   }
     463             : 
     464             :   private static String getLoginRedirectUrl(HttpServletRequest req) {
     465           0 :     String contextPath = req.getContextPath();
     466           0 :     String loginUrl = contextPath + "/login/";
     467           0 :     String token = req.getRequestURI();
     468           0 :     if (!contextPath.isEmpty()) {
     469           0 :       token = token.substring(contextPath.length());
     470             :     }
     471             : 
     472           0 :     String queryString = req.getQueryString();
     473           0 :     if (queryString != null && !queryString.isEmpty()) {
     474           0 :       token = token + "?" + queryString;
     475             :     }
     476           0 :     return (loginUrl + Url.encode(token));
     477             :   }
     478             : 
     479             :   private static Map<String, String> getParameters(HttpServletRequest req) {
     480           0 :     final Map<String, String> params = new HashMap<>();
     481           0 :     for (String pair : Splitter.on(CharMatcher.anyOf("&;")).split(req.getQueryString())) {
     482           0 :       final int eq = pair.indexOf('=');
     483           0 :       if (0 < eq) {
     484           0 :         String name = pair.substring(0, eq);
     485           0 :         String value = pair.substring(eq + 1);
     486             : 
     487           0 :         name = Url.decode(name);
     488           0 :         value = Url.decode(value);
     489           0 :         params.put(name, value);
     490             :       }
     491           0 :     }
     492           0 :     return params;
     493             :   }
     494             : 
     495             :   private void exec(HttpServletRequest req, HttpServletResponse rsp, ProjectState projectState)
     496             :       throws IOException {
     497             :     final Process proc =
     498           0 :         Runtime.getRuntime()
     499           0 :             .exec(
     500           0 :                 new String[] {gitwebCgi.toAbsolutePath().toString()},
     501           0 :                 makeEnv(req, projectState),
     502           0 :                 gitwebCgi.toAbsolutePath().getParent().toFile());
     503             : 
     504           0 :     copyStderrToLog(proc.getErrorStream());
     505           0 :     if (0 < req.getContentLength()) {
     506           0 :       copyContentToCGI(req, proc.getOutputStream());
     507             :     } else {
     508           0 :       proc.getOutputStream().close();
     509             :     }
     510             : 
     511           0 :     try (InputStream in = new BufferedInputStream(proc.getInputStream(), BUFFER_SIZE)) {
     512           0 :       readCgiHeaders(rsp, in);
     513             : 
     514           0 :       try (OutputStream out = rsp.getOutputStream()) {
     515           0 :         final byte[] buf = new byte[BUFFER_SIZE];
     516             :         int n;
     517           0 :         while ((n = in.read(buf)) > 0) {
     518           0 :           out.write(buf, 0, n);
     519             :         }
     520             :       }
     521           0 :     } catch (IOException e) {
     522             :       // The browser has probably closed its input stream. We don't
     523             :       // want to continue executing this request.
     524             :       //
     525           0 :       proc.destroy();
     526           0 :       return;
     527           0 :     }
     528             : 
     529             :     try {
     530           0 :       proc.waitFor();
     531             : 
     532           0 :       final int status = proc.exitValue();
     533           0 :       if (0 != status) {
     534           0 :         logger.atSevere().log("Non-zero exit status (%d) from %s", status, gitwebCgi);
     535           0 :         if (!rsp.isCommitted()) {
     536           0 :           rsp.sendError(500);
     537             :         }
     538             :       }
     539           0 :     } catch (InterruptedException ie) {
     540           0 :       logger.atFine().log("CGI: interrupted waiting for CGI to terminate");
     541           0 :     }
     542           0 :   }
     543             : 
     544             :   private String[] makeEnv(HttpServletRequest req, ProjectState projectState)
     545             :       throws RepositoryNotFoundException, IOException {
     546           0 :     final EnvList env = new EnvList(_env);
     547           0 :     final int contentLength = Math.max(0, req.getContentLength());
     548             : 
     549             :     // These ones are from "The WWW Common Gateway Interface Version 1.1"
     550             :     //
     551           0 :     env.set("AUTH_TYPE", req.getAuthType());
     552           0 :     env.set("CONTENT_LENGTH", Integer.toString(contentLength));
     553           0 :     env.set("CONTENT_TYPE", req.getContentType());
     554           0 :     env.set("GATEWAY_INTERFACE", "CGI/1.1");
     555           0 :     env.set("PATH_INFO", req.getPathInfo());
     556           0 :     env.set("PATH_TRANSLATED", null);
     557           0 :     env.set("QUERY_STRING", req.getQueryString());
     558           0 :     env.set("REMOTE_ADDR", req.getRemoteAddr());
     559           0 :     env.set("REMOTE_HOST", req.getRemoteHost());
     560           0 :     env.set("HTTPS", req.isSecure() ? "ON" : "OFF");
     561             : 
     562             :     // The identity information reported about the connection by a
     563             :     // RFC 1413 [11] request to the remote agent, if
     564             :     // available. Servers MAY choose not to support this feature, or
     565             :     // not to request the data for efficiency reasons.
     566             :     // "REMOTE_IDENT" => "NYI"
     567             :     //
     568           0 :     env.set("REQUEST_METHOD", req.getMethod());
     569           0 :     env.set("SCRIPT_NAME", req.getContextPath() + req.getServletPath());
     570           0 :     env.set("SCRIPT_FILENAME", gitwebCgi.toAbsolutePath().toString());
     571           0 :     env.set("SERVER_NAME", req.getServerName());
     572           0 :     env.set("SERVER_PORT", Integer.toString(req.getServerPort()));
     573           0 :     env.set("SERVER_PROTOCOL", req.getProtocol());
     574           0 :     env.set("SERVER_SOFTWARE", getServletContext().getServerInfo());
     575             : 
     576           0 :     for (String name : getHeaderNames(req)) {
     577           0 :       final String value = req.getHeader(name);
     578           0 :       env.set("HTTP_" + name.toUpperCase().replace('-', '_'), value);
     579           0 :     }
     580             : 
     581           0 :     Project.NameKey nameKey = projectState.getNameKey();
     582           0 :     env.set("GERRIT_CONTEXT_PATH", req.getContextPath() + "/");
     583           0 :     env.set("GERRIT_PROJECT_NAME", nameKey.get());
     584             : 
     585           0 :     env.set("GITWEB_PROJECTROOT", getProjectRoot(nameKey));
     586             : 
     587           0 :     if (projectState.statePermitsRead()
     588             :         && permissionBackend
     589           0 :             .user(anonymousUserProvider.get())
     590           0 :             .project(nameKey)
     591           0 :             .testOrFalse(ProjectPermission.READ)) {
     592           0 :       env.set("GERRIT_ANONYMOUS_READ", "1");
     593             :     }
     594             : 
     595           0 :     String remoteUser = null;
     596           0 :     if (userProvider.get().isIdentifiedUser()) {
     597           0 :       IdentifiedUser u = userProvider.get().asIdentifiedUser();
     598           0 :       Optional<String> user = u.getUserName();
     599           0 :       env.set("GERRIT_USER_NAME", user.orElse(null));
     600           0 :       remoteUser = user.orElseGet(() -> "account-" + u.getAccountId());
     601             :     }
     602           0 :     env.set("REMOTE_USER", remoteUser);
     603             : 
     604             :     // Override CGI settings using alternative URI provided by gitweb.url.
     605             :     // This is required to trick gitweb into thinking that it's served under
     606             :     // different URL. Setting just $my_uri on the perl's side isn't enough,
     607             :     // because few actions (atom, blobdiff_plain, commitdiff_plain) rely on
     608             :     // URL returned by $cgi->self_url().
     609             :     //
     610           0 :     if (gitwebUrl != null) {
     611           0 :       int schemePort = -1;
     612             : 
     613           0 :       if (gitwebUrl.getScheme() != null) {
     614           0 :         if (gitwebUrl.getScheme().equals("http")) {
     615           0 :           env.set("HTTPS", "OFF");
     616           0 :           schemePort = 80;
     617             :         } else {
     618           0 :           env.set("HTTPS", "ON");
     619           0 :           schemePort = 443;
     620             :         }
     621             :       }
     622             : 
     623           0 :       if (gitwebUrl.getHost() != null) {
     624           0 :         env.set("SERVER_NAME", gitwebUrl.getHost());
     625           0 :         env.set("HTTP_HOST", gitwebUrl.getHost());
     626             :       }
     627             : 
     628           0 :       if (gitwebUrl.getPort() != -1) {
     629           0 :         env.set("SERVER_PORT", Integer.toString(gitwebUrl.getPort()));
     630           0 :       } else if (schemePort != -1) {
     631           0 :         env.set("SERVER_PORT", Integer.toString(schemePort));
     632             :       }
     633             : 
     634           0 :       if (gitwebUrl.getPath() != null) {
     635           0 :         env.set("SCRIPT_NAME", gitwebUrl.getPath().isEmpty() ? "/" : gitwebUrl.getPath());
     636             :       }
     637             :     }
     638             : 
     639           0 :     return env.getEnvArray();
     640             :   }
     641             : 
     642             :   private String getProjectRoot(Project.NameKey nameKey)
     643             :       throws RepositoryNotFoundException, IOException {
     644           0 :     try (Repository repo = repoManager.openRepository(nameKey)) {
     645           0 :       return getProjectRoot(repo);
     646             :     }
     647             :   }
     648             : 
     649             :   private String getProjectRoot(Repository repo) {
     650           0 :     if (repo instanceof DelegateRepository) {
     651           0 :       return getProjectRoot(((DelegateRepository) repo).delegate());
     652             :     }
     653             : 
     654           0 :     if (repo instanceof FileRepository) {
     655           0 :       return repo.getDirectory().getAbsolutePath();
     656             :     }
     657             : 
     658           0 :     throw new ProvisionException("Gitweb can only be used with FileRepository");
     659             :   }
     660             : 
     661             :   private void copyContentToCGI(HttpServletRequest req, OutputStream dst) throws IOException {
     662           0 :     final int contentLength = req.getContentLength();
     663           0 :     final InputStream src = req.getInputStream();
     664           0 :     new Thread(
     665             :             () -> {
     666             :               try {
     667             :                 try {
     668           0 :                   final byte[] buf = new byte[BUFFER_SIZE];
     669           0 :                   int remaining = contentLength;
     670           0 :                   while (0 < remaining) {
     671           0 :                     final int max = Math.max(buf.length, remaining);
     672           0 :                     final int n = src.read(buf, 0, max);
     673           0 :                     if (n < 0) {
     674           0 :                       throw new EOFException("Expected " + remaining + " more bytes");
     675             :                     }
     676           0 :                     dst.write(buf, 0, n);
     677           0 :                     remaining -= n;
     678           0 :                   }
     679             :                 } finally {
     680           0 :                   dst.close();
     681             :                 }
     682           0 :               } catch (IOException e) {
     683           0 :                 logger.atSevere().withCause(e).log("Unexpected error copying input to CGI");
     684           0 :               }
     685           0 :             },
     686             :             "Gitweb-InputFeeder")
     687           0 :         .start();
     688           0 :   }
     689             : 
     690             :   private void copyStderrToLog(InputStream in) {
     691           0 :     new Thread(
     692             :             () -> {
     693           0 :               try (BufferedReader br =
     694           0 :                   new BufferedReader(new InputStreamReader(in, ISO_8859_1.name()))) {
     695           0 :                 String err =
     696           0 :                     br.lines()
     697           0 :                         .filter(s -> !s.isEmpty())
     698           0 :                         .map(s -> "CGI: " + s)
     699           0 :                         .collect(Collectors.joining("\n"))
     700           0 :                         .trim();
     701           0 :                 if (!err.isEmpty()) {
     702           0 :                   logger.atSevere().log("%s", err);
     703             :                 }
     704           0 :               } catch (IOException e) {
     705           0 :                 logger.atSevere().withCause(e).log("Unexpected error copying stderr from CGI");
     706           0 :               }
     707           0 :             },
     708             :             "Gitweb-ErrorLogger")
     709           0 :         .start();
     710           0 :   }
     711             : 
     712             :   private void readCgiHeaders(HttpServletResponse res, InputStream in) throws IOException {
     713             :     String line;
     714           0 :     while (!(line = readLine(in)).isEmpty()) {
     715           0 :       if (line.startsWith("HTTP")) {
     716             :         // CGI believes it is a non-parsed-header CGI. We refuse
     717             :         // to support that here so abort.
     718             :         //
     719           0 :         throw new IOException("NPH CGI not supported: " + line);
     720             :       }
     721             : 
     722           0 :       final int sep = line.indexOf(':');
     723           0 :       if (sep < 0) {
     724           0 :         throw new IOException("CGI returned invalid header: " + line);
     725             :       }
     726             : 
     727           0 :       final String key = line.substring(0, sep).trim();
     728           0 :       final String value = line.substring(sep + 1).trim();
     729           0 :       if ("Location".equalsIgnoreCase(key)) {
     730           0 :         res.sendRedirect(value);
     731             : 
     732           0 :       } else if ("Status".equalsIgnoreCase(key)) {
     733           0 :         final List<String> token = Splitter.on(' ').splitToList(value);
     734           0 :         final int status = Integer.parseInt(token.get(0));
     735           0 :         res.setStatus(status);
     736             : 
     737           0 :       } else {
     738           0 :         res.addHeader(key, value);
     739             :       }
     740           0 :     }
     741           0 :   }
     742             : 
     743             :   private String readLine(InputStream in) throws IOException {
     744           0 :     final StringBuilder buf = new StringBuilder();
     745             :     int b;
     746           0 :     while ((b = in.read()) != -1 && b != '\n') {
     747           0 :       buf.append((char) b);
     748             :     }
     749           0 :     return buf.toString().trim();
     750             :   }
     751             : 
     752             :   @SuppressWarnings("JdkObsolete")
     753             :   private static Iterable<String> getHeaderNames(HttpServletRequest req) {
     754           0 :     return Collections.list(req.getHeaderNames());
     755             :   }
     756             : 
     757             :   /** private utility class that manages the Environment passed to exec. */
     758             :   private static class EnvList {
     759             :     private Map<String, String> envMap;
     760             : 
     761           0 :     EnvList() {
     762           0 :       envMap = new HashMap<>();
     763           0 :     }
     764             : 
     765           0 :     EnvList(EnvList l) {
     766           0 :       envMap = new HashMap<>(l.envMap);
     767           0 :     }
     768             : 
     769             :     /** Set a name/value pair, null values will be treated as an empty String */
     770             :     public void set(String name, String value) {
     771           0 :       if (value == null) {
     772           0 :         value = "";
     773             :       }
     774           0 :       envMap.put(name, name + "=" + value);
     775           0 :     }
     776             : 
     777             :     /** Get representation suitable for passing to exec. */
     778             :     public String[] getEnvArray() {
     779           0 :       return envMap.values().toArray(new String[envMap.size()]);
     780             :     }
     781             : 
     782             :     @Override
     783             :     public String toString() {
     784           0 :       return envMap.toString();
     785             :     }
     786             :   }
     787             : }

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