| // 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(); | |
| } | |
| } | |
| } |