| // Copyright (C) 2009 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // CGI environment and execution management portions are: |
| // |
| // ======================================================================== |
| // Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| |
| package com.google.gerrit.httpd.gitweb; |
| |
| import static java.nio.charset.StandardCharsets.ISO_8859_1; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.CharMatcher; |
| import com.google.common.base.Splitter; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.common.PageLinks; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.extensions.restapi.Url; |
| import com.google.gerrit.server.AnonymousUser; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.config.AllProjectsName; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| import com.google.gerrit.server.config.GitwebCgiConfig; |
| import com.google.gerrit.server.config.GitwebConfig; |
| import com.google.gerrit.server.config.SitePaths; |
| import com.google.gerrit.server.git.DelegateRepository; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.permissions.PermissionBackend; |
| import com.google.gerrit.server.permissions.PermissionBackendException; |
| import com.google.gerrit.server.permissions.ProjectPermission; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.gerrit.server.ssh.SshInfo; |
| import com.google.gerrit.util.http.CacheHeaders; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.ProvisionException; |
| import com.google.inject.Singleton; |
| import java.io.BufferedInputStream; |
| import java.io.BufferedReader; |
| import java.io.EOFException; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.io.PrintWriter; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| import javax.servlet.http.HttpServlet; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.internal.storage.file.FileRepository; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.Repository; |
| |
| /** Invokes {@code gitweb.cgi} for the project given in {@code p}. */ |
| @Singleton |
| class GitwebServlet extends HttpServlet { |
| private static final long serialVersionUID = 1L; |
| |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private static final String PROJECT_LIST_ACTION = "project_list"; |
| private static final int BUFFER_SIZE = 8192; |
| |
| private final Set<String> deniedActions; |
| private final Path gitwebCgi; |
| private final URI gitwebUrl; |
| private final GitRepositoryManager repoManager; |
| private final ProjectCache projectCache; |
| private final PermissionBackend permissionBackend; |
| private final Provider<AnonymousUser> anonymousUserProvider; |
| private final Provider<CurrentUser> userProvider; |
| private final EnvList _env; |
| |
| @SuppressWarnings("CheckReturnValue") |
| @Inject |
| GitwebServlet( |
| GitRepositoryManager repoManager, |
| ProjectCache projectCache, |
| PermissionBackend permissionBackend, |
| Provider<CurrentUser> userProvider, |
| SitePaths site, |
| @GerritServerConfig Config cfg, |
| SshInfo sshInfo, |
| Provider<AnonymousUser> anonymousUserProvider, |
| GitwebConfig gitwebConfig, |
| GitwebCgiConfig gitwebCgiConfig, |
| AllProjectsName allProjects) |
| throws IOException { |
| this.repoManager = repoManager; |
| this.projectCache = projectCache; |
| this.permissionBackend = permissionBackend; |
| this.anonymousUserProvider = anonymousUserProvider; |
| this.userProvider = userProvider; |
| this.gitwebCgi = gitwebCgiConfig.getGitwebCgi(); |
| this.deniedActions = new HashSet<>(); |
| |
| // ensure that Gitweb works on supported repository type by checking All-Projects project |
| getProjectRoot(allProjects); |
| |
| final String url = gitwebConfig.getUrl(); |
| if (url != null && !url.equals("gitweb")) { |
| URI uri = null; |
| try { |
| uri = new URI(url); |
| } catch (URISyntaxException e) { |
| logger.atSevere().log("Invalid gitweb.url: %s", url); |
| } |
| gitwebUrl = uri; |
| } else { |
| gitwebUrl = null; |
| } |
| |
| deniedActions.add("forks"); |
| deniedActions.add("opml"); |
| deniedActions.add("project_index"); |
| |
| _env = new EnvList(); |
| makeSiteConfig(site, cfg, sshInfo); |
| |
| if (!_env.envMap.containsKey("SystemRoot")) { |
| String os = System.getProperty("os.name"); |
| if (os != null && os.toLowerCase(Locale.US).contains("windows")) { |
| String sysroot = System.getenv("SystemRoot"); |
| if (sysroot == null || sysroot.isEmpty()) { |
| sysroot = "C:\\WINDOWS"; |
| } |
| _env.set("SystemRoot", sysroot); |
| } |
| } |
| |
| if (!_env.envMap.containsKey("PATH")) { |
| _env.set("PATH", System.getenv("PATH")); |
| } |
| } |
| |
| private void makeSiteConfig(SitePaths site, Config cfg, SshInfo sshInfo) throws IOException { |
| if (!Files.exists(site.tmp_dir)) { |
| Files.createDirectories(site.tmp_dir); |
| } |
| Path myconf = Files.createTempFile(site.tmp_dir, "gitweb_config", ".perl"); |
| |
| // To make our configuration file only readable or writable by us; this reduces the chances of |
| // someone tampering with the file. |
| File myconfFile = myconf.toFile(); |
| myconfFile.setWritable(false, false /* all */); |
| myconfFile.setReadable(false, false /* all */); |
| myconfFile.setExecutable(false, false /* all */); |
| |
| myconfFile.setWritable(true, true /* owner only */); |
| myconfFile.setReadable(true, true /* owner only */); |
| |
| myconfFile.deleteOnExit(); |
| |
| _env.set("GIT_DIR", "."); |
| _env.set("GITWEB_CONFIG", myconf.toAbsolutePath().toString()); |
| |
| try (PrintWriter p = new PrintWriter(Files.newBufferedWriter(myconf, UTF_8))) { |
| p.print("# Autogenerated by Gerrit Code Review \n"); |
| p.print("# DO NOT EDIT\n"); |
| p.print("\n"); |
| |
| // We are mounted at the same level in the context as the main |
| // UI, so we can include the same header and footer scheme. |
| // |
| Path hdr = site.site_header; |
| if (Files.isRegularFile(hdr)) { |
| p.print("$site_header = " + quoteForPerl(hdr) + ";\n"); |
| } |
| Path ftr = site.site_footer; |
| if (Files.isRegularFile(ftr)) { |
| p.print("$site_footer = " + quoteForPerl(ftr) + ";\n"); |
| } |
| |
| // Top level should return to Gerrit's UI. |
| // |
| p.print("$home_link = $ENV{'GERRIT_CONTEXT_PATH'};\n"); |
| p.print("$home_link_str = 'Code Review';\n"); |
| |
| p.print("$favicon = 'favicon.ico';\n"); |
| p.print("$logo = 'gitweb-logo.png';\n"); |
| p.print("$javascript = 'gitweb.js';\n"); |
| p.print("@stylesheets = ('gitweb-default.css');\n"); |
| Path css = site.site_css; |
| if (Files.isRegularFile(css)) { |
| p.print("push @stylesheets, 'gitweb-site.css';\n"); |
| } |
| |
| // Try to make the title match Gerrit's normal window title |
| // scheme of host followed by 'Code Review'. |
| // |
| p.print("$site_name = $home_link_str;\n"); |
| p.print("$site_name = qq{$1 $site_name} if "); |
| p.print("$ENV{'SERVER_NAME'} =~ m,^([^.]+(?:\\.[^.]+)?)(?:\\.|$),;\n"); |
| |
| // Assume by default that XSS is a problem, and try to prevent it. |
| // |
| p.print("$prevent_xss = 1;\n"); |
| |
| // Generate URLs using smart http:// |
| // |
| p.print("{\n"); |
| p.print(" my $secure = $ENV{'HTTPS'} =~ /^ON$/i;\n"); |
| p.print(" my $http_url = $secure ? 'https://' : 'http://';\n"); |
| p.print(" $http_url .= qq{$ENV{'GERRIT_USER_NAME'}@}\n"); |
| p.print(" unless $ENV{'GERRIT_ANONYMOUS_READ'};\n"); |
| p.print(" $http_url .= $ENV{'SERVER_NAME'};\n"); |
| p.print(" $http_url .= qq{:$ENV{'SERVER_PORT'}}\n"); |
| p.print(" if (( $secure && $ENV{'SERVER_PORT'} != 443)\n"); |
| p.print(" || (!$secure && $ENV{'SERVER_PORT'} != 80)\n"); |
| p.print(" );\n"); |
| p.print(" my $context = $ENV{'GERRIT_CONTEXT_PATH'};\n"); |
| p.print(" chop($context);\n"); |
| p.print(" $http_url .= qq{$context};\n"); |
| p.print(" $http_url .= qq{/a}\n"); |
| p.print(" unless $ENV{'GERRIT_ANONYMOUS_READ'};\n"); |
| p.print(" push @git_base_url_list, $http_url;\n"); |
| p.print("}\n"); |
| |
| // Generate URLs using anonymous git:// |
| // |
| String url = cfg.getString("gerrit", null, "canonicalGitUrl"); |
| if (url != null) { |
| if (url.endsWith("/")) { |
| url = url.substring(0, url.length() - 1); |
| } |
| p.print("if ($ENV{'GERRIT_ANONYMOUS_READ'}) {\n"); |
| p.print(" push @git_base_url_list, "); |
| p.print(quoteForPerl(url)); |
| p.print(";\n"); |
| p.print("}\n"); |
| } |
| |
| // Generate URLs using authenticated ssh:// |
| // |
| if (sshInfo != null && !sshInfo.getHostKeys().isEmpty()) { |
| String sshAddr = sshInfo.getHostKeys().get(0).getHost(); |
| p.print("if ($ENV{'GERRIT_USER_NAME'}) {\n"); |
| p.print(" push @git_base_url_list, join('', 'ssh://'"); |
| p.print(", $ENV{'GERRIT_USER_NAME'}"); |
| p.print(", '@'"); |
| if (sshAddr.startsWith("*:") || "".equals(sshAddr)) { |
| p.print(", $ENV{'SERVER_NAME'}"); |
| } |
| if (sshAddr.startsWith("*")) { |
| sshAddr = sshAddr.substring(1); |
| } |
| p.print(", " + quoteForPerl(sshAddr)); |
| p.print(");\n"); |
| p.print("}\n"); |
| } |
| |
| // Link back to Gerrit (when possible, to matching review record). |
| // Supported gitweb's hash values are: |
| // - (missing), |
| // - HEAD, |
| // - refs/heads/<branch>, |
| // - refs/changes/*/<change>/*, |
| // - <revision>. |
| // |
| p.print("sub add_review_link {\n"); |
| p.print(" my $h = shift;\n"); |
| p.print(" my $q;\n"); |
| p.print(" if (!$h || $h eq 'HEAD') {\n"); |
| p.print(" $q = qq{#/q/project:$ENV{'GERRIT_PROJECT_NAME'}};\n"); |
| p.print(" } elsif ($h =~ /^refs\\/heads\\/([-\\w]+)$/) {\n"); |
| p.print(" $q = qq{#/q/project:$ENV{'GERRIT_PROJECT_NAME'}"); |
| p.print("+branch:$1};\n"); // wrapped |
| p.print(" } elsif ($h =~ /^refs\\/changes\\/\\d{2}\\/(\\d+)\\/\\d+$/) "); |
| p.print("{\n"); // wrapped |
| p.print(" $q = qq{#/c/$1};\n"); |
| p.print(" } else {\n"); |
| p.print(" $q = qq{#/q/$h};\n"); |
| p.print(" }\n"); |
| p.print(" my $r = qq{$ENV{'GERRIT_CONTEXT_PATH'}$q};\n"); |
| p.print(" push @{$feature{'actions'}{'default'}},\n"); |
| p.print(" ('review',$r,'commitdiff');\n"); |
| p.print("}\n"); |
| p.print("if ($cgi->param('hb')) {\n"); |
| p.print(" add_review_link(scalar $cgi->param('hb'));\n"); |
| p.print("} elsif ($cgi->param('h')) {\n"); |
| p.print(" add_review_link(scalar $cgi->param('h'));\n"); |
| p.print("} else {\n"); |
| p.print(" add_review_link();\n"); |
| p.print("}\n"); |
| |
| // If the administrator has created a site-specific gitweb_config, |
| // load that before we perform any final overrides. |
| // |
| Path sitecfg = site.site_gitweb; |
| if (Files.isRegularFile(sitecfg)) { |
| p.print("$GITWEB_CONFIG = " + quoteForPerl(sitecfg) + ";\n"); |
| p.print("if (-e $GITWEB_CONFIG) {\n"); |
| p.print(" do " + quoteForPerl(sitecfg) + ";\n"); |
| p.print("}\n"); |
| } |
| |
| p.print("$projectroot = $ENV{'GITWEB_PROJECTROOT'};\n"); |
| |
| // Permit exporting only the project we were started for. |
| // We use the name under $projectroot in case symlinks |
| // were involved in the path. |
| // |
| p.print("$export_auth_hook = sub {\n"); |
| p.print(" my $dir = shift;\n"); |
| p.print(" my $name = $ENV{'GERRIT_PROJECT_NAME'};\n"); |
| p.print(" my $allow = qq{$projectroot/$name.git};\n"); |
| p.print(" return $dir eq $allow;\n"); |
| p.print(" };\n"); |
| |
| // Do not allow the administrator to enable path info, its |
| // not a URL format we currently support. |
| // |
| p.print("$feature{'pathinfo'}{'override'} = 0;\n"); |
| p.print("$feature{'pathinfo'}{'default'} = [0];\n"); |
| |
| // We don't do forking, so don't allow it to be enabled. |
| // |
| p.print("$feature{'forks'}{'override'} = 0;\n"); |
| p.print("$feature{'forks'}{'default'} = [0];\n"); |
| } |
| |
| myconfFile.setReadOnly(); |
| } |
| |
| private static String quoteForPerl(Path value) { |
| return quoteForPerl(value.toAbsolutePath().toString()); |
| } |
| |
| private static String quoteForPerl(String value) { |
| if (value == null || value.isEmpty()) { |
| return "''"; |
| } |
| if (!value.contains("'")) { |
| return "'" + value + "'"; |
| } |
| if (!value.contains("{") && !value.contains("}")) { |
| return "q{" + value + "}"; |
| } |
| throw new IllegalArgumentException("Cannot quote in Perl: " + value); |
| } |
| |
| @Override |
| protected void service(HttpServletRequest req, HttpServletResponse rsp) throws IOException { |
| if (req.getQueryString() == null || req.getQueryString().isEmpty()) { |
| // No query string? They want the project list, which we don't |
| // currently support. Return to Gerrit's own web UI. |
| // |
| rsp.sendRedirect(req.getContextPath() + "/"); |
| return; |
| } |
| |
| final Map<String, String> params = getParameters(req); |
| String a = params.get("a"); |
| if (a != null) { |
| if (deniedActions.contains(a)) { |
| rsp.sendError(HttpServletResponse.SC_FORBIDDEN); |
| return; |
| } |
| |
| if (a.equals(PROJECT_LIST_ACTION)) { |
| rsp.sendRedirect( |
| req.getContextPath() |
| + "/#" |
| + PageLinks.ADMIN_PROJECTS |
| + "?filter=" |
| + Url.encode(params.get("pf") + "/")); |
| return; |
| } |
| } |
| |
| String name = params.get("p"); |
| if (name == null) { |
| rsp.sendError(HttpServletResponse.SC_NOT_FOUND); |
| return; |
| } |
| if (name.endsWith(".git")) { |
| name = name.substring(0, name.length() - 4); |
| } |
| |
| Project.NameKey nameKey = Project.nameKey(name); |
| Optional<ProjectState> projectState; |
| try { |
| projectState = projectCache.get(nameKey); |
| if (!projectState.isPresent()) { |
| sendErrorOrRedirect(req, rsp, HttpServletResponse.SC_NOT_FOUND); |
| return; |
| } |
| |
| projectState.get().checkStatePermitsRead(); |
| permissionBackend.user(userProvider.get()).project(nameKey).check(ProjectPermission.READ); |
| } catch (AuthException e) { |
| sendErrorOrRedirect(req, rsp, HttpServletResponse.SC_NOT_FOUND); |
| return; |
| } catch (IOException | PermissionBackendException err) { |
| logger.atSevere().withCause(err).log("cannot load %s", name); |
| rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); |
| return; |
| } catch (ResourceConflictException e) { |
| sendErrorOrRedirect(req, rsp, HttpServletResponse.SC_CONFLICT); |
| return; |
| } |
| |
| try (Repository repo = repoManager.openRepository(nameKey)) { |
| CacheHeaders.setNotCacheable(rsp); |
| exec(req, rsp, projectState.get()); |
| } catch (RepositoryNotFoundException e) { |
| getServletContext().log("Cannot open repository", e); |
| rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); |
| } |
| } |
| |
| /** |
| * Sends error response if the user is authenticated. Or redirect the user to the login page. By |
| * doing this, anonymous users cannot infer the existence of a resource from the status code. |
| */ |
| private void sendErrorOrRedirect(HttpServletRequest req, HttpServletResponse rsp, int statusCode) |
| throws IOException { |
| if (userProvider.get().isIdentifiedUser()) { |
| rsp.sendError(statusCode); |
| } else { |
| rsp.sendRedirect(getLoginRedirectUrl(req)); |
| } |
| } |
| |
| private static String getLoginRedirectUrl(HttpServletRequest req) { |
| String contextPath = req.getContextPath(); |
| String loginUrl = contextPath + "/login/"; |
| String token = req.getRequestURI(); |
| if (!contextPath.isEmpty()) { |
| token = token.substring(contextPath.length()); |
| } |
| |
| String queryString = req.getQueryString(); |
| if (queryString != null && !queryString.isEmpty()) { |
| token = token + "?" + queryString; |
| } |
| return (loginUrl + Url.encode(token)); |
| } |
| |
| private static Map<String, String> getParameters(HttpServletRequest req) { |
| final Map<String, String> params = new HashMap<>(); |
| for (String pair : Splitter.on(CharMatcher.anyOf("&;")).split(req.getQueryString())) { |
| final int eq = pair.indexOf('='); |
| if (0 < eq) { |
| String name = pair.substring(0, eq); |
| String value = pair.substring(eq + 1); |
| |
| name = Url.decode(name); |
| value = Url.decode(value); |
| params.put(name, value); |
| } |
| } |
| return params; |
| } |
| |
| private void exec(HttpServletRequest req, HttpServletResponse rsp, ProjectState projectState) |
| throws IOException { |
| final Process proc = |
| Runtime.getRuntime() |
| .exec( |
| new String[] {gitwebCgi.toAbsolutePath().toString()}, |
| makeEnv(req, projectState), |
| gitwebCgi.toAbsolutePath().getParent().toFile()); |
| |
| copyStderrToLog(proc.getErrorStream()); |
| if (0 < req.getContentLength()) { |
| copyContentToCGI(req, proc.getOutputStream()); |
| } else { |
| proc.getOutputStream().close(); |
| } |
| |
| try (InputStream in = new BufferedInputStream(proc.getInputStream(), BUFFER_SIZE)) { |
| readCgiHeaders(rsp, in); |
| |
| try (OutputStream out = rsp.getOutputStream()) { |
| final byte[] buf = new byte[BUFFER_SIZE]; |
| int n; |
| while ((n = in.read(buf)) > 0) { |
| out.write(buf, 0, n); |
| } |
| } |
| } catch (IOException e) { |
| // The browser has probably closed its input stream. We don't |
| // want to continue executing this request. |
| // |
| proc.destroy(); |
| return; |
| } |
| |
| try { |
| proc.waitFor(); |
| |
| final int status = proc.exitValue(); |
| if (0 != status) { |
| logger.atSevere().log("Non-zero exit status (%d) from %s", status, gitwebCgi); |
| if (!rsp.isCommitted()) { |
| rsp.sendError(500); |
| } |
| } |
| } catch (InterruptedException ie) { |
| logger.atFine().log("CGI: interrupted waiting for CGI to terminate"); |
| } |
| } |
| |
| private String[] makeEnv(HttpServletRequest req, ProjectState projectState) |
| throws RepositoryNotFoundException, IOException { |
| final EnvList env = new EnvList(_env); |
| final int contentLength = Math.max(0, req.getContentLength()); |
| |
| // These ones are from "The WWW Common Gateway Interface Version 1.1" |
| // |
| env.set("AUTH_TYPE", req.getAuthType()); |
| env.set("CONTENT_LENGTH", Integer.toString(contentLength)); |
| env.set("CONTENT_TYPE", req.getContentType()); |
| env.set("GATEWAY_INTERFACE", "CGI/1.1"); |
| env.set("PATH_INFO", req.getPathInfo()); |
| env.set("PATH_TRANSLATED", null); |
| env.set("QUERY_STRING", req.getQueryString()); |
| env.set("REMOTE_ADDR", req.getRemoteAddr()); |
| env.set("REMOTE_HOST", req.getRemoteHost()); |
| env.set("HTTPS", req.isSecure() ? "ON" : "OFF"); |
| |
| // The identity information reported about the connection by a |
| // RFC 1413 [11] request to the remote agent, if |
| // available. Servers MAY choose not to support this feature, or |
| // not to request the data for efficiency reasons. |
| // "REMOTE_IDENT" => "NYI" |
| // |
| env.set("REQUEST_METHOD", req.getMethod()); |
| env.set("SCRIPT_NAME", req.getContextPath() + req.getServletPath()); |
| env.set("SCRIPT_FILENAME", gitwebCgi.toAbsolutePath().toString()); |
| env.set("SERVER_NAME", req.getServerName()); |
| env.set("SERVER_PORT", Integer.toString(req.getServerPort())); |
| env.set("SERVER_PROTOCOL", req.getProtocol()); |
| env.set("SERVER_SOFTWARE", getServletContext().getServerInfo()); |
| |
| for (String name : getHeaderNames(req)) { |
| final String value = req.getHeader(name); |
| env.set("HTTP_" + name.toUpperCase(Locale.US).replace('-', '_'), value); |
| } |
| |
| Project.NameKey nameKey = projectState.getNameKey(); |
| env.set("GERRIT_CONTEXT_PATH", req.getContextPath() + "/"); |
| env.set("GERRIT_PROJECT_NAME", nameKey.get()); |
| |
| env.set("GITWEB_PROJECTROOT", getProjectRoot(nameKey)); |
| |
| if (projectState.statePermitsRead() |
| && permissionBackend |
| .user(anonymousUserProvider.get()) |
| .project(nameKey) |
| .testOrFalse(ProjectPermission.READ)) { |
| env.set("GERRIT_ANONYMOUS_READ", "1"); |
| } |
| |
| String remoteUser = null; |
| if (userProvider.get().isIdentifiedUser()) { |
| IdentifiedUser u = userProvider.get().asIdentifiedUser(); |
| Optional<String> user = u.getUserName(); |
| env.set("GERRIT_USER_NAME", user.orElse(null)); |
| remoteUser = user.orElseGet(() -> "account-" + u.getAccountId()); |
| } |
| env.set("REMOTE_USER", remoteUser); |
| |
| // Override CGI settings using alternative URI provided by gitweb.url. |
| // This is required to trick gitweb into thinking that it's served under |
| // different URL. Setting just $my_uri on the perl's side isn't enough, |
| // because few actions (atom, blobdiff_plain, commitdiff_plain) rely on |
| // URL returned by $cgi->self_url(). |
| // |
| if (gitwebUrl != null) { |
| int schemePort = -1; |
| |
| if (gitwebUrl.getScheme() != null) { |
| if (gitwebUrl.getScheme().equals("http")) { |
| env.set("HTTPS", "OFF"); |
| schemePort = 80; |
| } else { |
| env.set("HTTPS", "ON"); |
| schemePort = 443; |
| } |
| } |
| |
| if (gitwebUrl.getHost() != null) { |
| env.set("SERVER_NAME", gitwebUrl.getHost()); |
| env.set("HTTP_HOST", gitwebUrl.getHost()); |
| } |
| |
| if (gitwebUrl.getPort() != -1) { |
| env.set("SERVER_PORT", Integer.toString(gitwebUrl.getPort())); |
| } else if (schemePort != -1) { |
| env.set("SERVER_PORT", Integer.toString(schemePort)); |
| } |
| |
| if (gitwebUrl.getPath() != null) { |
| env.set("SCRIPT_NAME", gitwebUrl.getPath().isEmpty() ? "/" : gitwebUrl.getPath()); |
| } |
| } |
| |
| return env.getEnvArray(); |
| } |
| |
| /** |
| * Return the project root under which the specified project is stored. |
| * |
| * @param nameKey the name of the project |
| * @return base directory |
| */ |
| @VisibleForTesting |
| String getProjectRoot(Project.NameKey nameKey) throws RepositoryNotFoundException, IOException { |
| try (Repository repo = repoManager.openRepository(nameKey)) { |
| return getRepositoryRoot(repo, nameKey).toString(); |
| } |
| } |
| |
| /** |
| * Return the repository root under which the specified repository is stored. |
| * |
| * @param repo the name of the repository |
| * @param nameKey project name |
| * @return base path |
| * @throws ProvisionException if the repo is not DelegateRepository or FileRepository. |
| */ |
| private static Path getRepositoryRoot(Repository repo, Project.NameKey nameKey) { |
| if (repo instanceof DelegateRepository) { |
| return getRepositoryRoot(((DelegateRepository) repo).delegate(), nameKey); |
| } |
| |
| if (repo instanceof FileRepository) { |
| String name = nameKey.get(); |
| Path current = repo.getDirectory().toPath(); |
| for (int i = 0; i <= CharMatcher.is('/').countIn(name); i++) { |
| current = current.getParent(); |
| } |
| return current; |
| } |
| |
| throw new ProvisionException("Gitweb can only be used with FileRepository"); |
| } |
| |
| private void copyContentToCGI(HttpServletRequest req, OutputStream dst) throws IOException { |
| final int contentLength = req.getContentLength(); |
| final InputStream src = req.getInputStream(); |
| new Thread( |
| () -> { |
| try { |
| try { |
| final byte[] buf = new byte[BUFFER_SIZE]; |
| int remaining = contentLength; |
| while (0 < remaining) { |
| final int max = Math.max(buf.length, remaining); |
| final int n = src.read(buf, 0, max); |
| if (n < 0) { |
| throw new EOFException("Expected " + remaining + " more bytes"); |
| } |
| dst.write(buf, 0, n); |
| remaining -= n; |
| } |
| } finally { |
| dst.close(); |
| } |
| } catch (IOException e) { |
| logger.atSevere().withCause(e).log("Unexpected error copying input to CGI"); |
| } |
| }, |
| "Gitweb-InputFeeder") |
| .start(); |
| } |
| |
| private void copyStderrToLog(InputStream in) { |
| new Thread( |
| () -> { |
| try (BufferedReader br = |
| new BufferedReader(new InputStreamReader(in, ISO_8859_1.name()))) { |
| String err = |
| br.lines() |
| .filter(s -> !s.isEmpty()) |
| .map(s -> "CGI: " + s) |
| .collect(Collectors.joining("\n")) |
| .trim(); |
| if (!err.isEmpty()) { |
| logger.atSevere().log("%s", err); |
| } |
| } catch (IOException e) { |
| logger.atSevere().withCause(e).log("Unexpected error copying stderr from CGI"); |
| } |
| }, |
| "Gitweb-ErrorLogger") |
| .start(); |
| } |
| |
| private void readCgiHeaders(HttpServletResponse res, InputStream in) throws IOException { |
| String line; |
| while (!(line = readLine(in)).isEmpty()) { |
| if (line.startsWith("HTTP")) { |
| // CGI believes it is a non-parsed-header CGI. We refuse |
| // to support that here so abort. |
| // |
| throw new IOException("NPH CGI not supported: " + line); |
| } |
| |
| final int sep = line.indexOf(':'); |
| if (sep < 0) { |
| throw new IOException("CGI returned invalid header: " + line); |
| } |
| |
| final String key = line.substring(0, sep).trim(); |
| final String value = line.substring(sep + 1).trim(); |
| if ("Location".equalsIgnoreCase(key)) { |
| res.sendRedirect(value); |
| |
| } else if ("Status".equalsIgnoreCase(key)) { |
| final List<String> token = Splitter.on(' ').splitToList(value); |
| final int status = Integer.parseInt(token.get(0)); |
| res.setStatus(status); |
| |
| } else { |
| res.addHeader(key, value); |
| } |
| } |
| } |
| |
| private String readLine(InputStream in) throws IOException { |
| final StringBuilder buf = new StringBuilder(); |
| int b; |
| while ((b = in.read()) != -1 && b != '\n') { |
| buf.append((char) b); |
| } |
| return buf.toString().trim(); |
| } |
| |
| @SuppressWarnings("JdkObsolete") |
| private static Iterable<String> getHeaderNames(HttpServletRequest req) { |
| return Collections.list(req.getHeaderNames()); |
| } |
| |
| /** private utility class that manages the Environment passed to exec. */ |
| private static class EnvList { |
| private Map<String, String> envMap; |
| |
| EnvList() { |
| envMap = new HashMap<>(); |
| } |
| |
| EnvList(EnvList l) { |
| envMap = new HashMap<>(l.envMap); |
| } |
| |
| /** Set a name/value pair, null values will be treated as an empty String */ |
| public void set(String name, String value) { |
| if (value == null) { |
| value = ""; |
| } |
| envMap.put(name, name + "=" + value); |
| } |
| |
| /** Get representation suitable for passing to exec. */ |
| public String[] getEnvArray() { |
| return envMap.values().toArray(new String[envMap.size()]); |
| } |
| |
| @Override |
| public String toString() { |
| return envMap.toString(); |
| } |
| } |
| } |