
This is the first part of the migration to Flogger. This change migrates all classes of the 'http' module to Flogger. Other modules continue to use slf4j. They should be migrated by follow-up changes. During this migration we try to make the log statements more consistent: - avoid string concatenation - avoid usage of String.format(...) Change-Id: I473c41733b00aa1ceab92fe0dc8cd1c6b347174c Signed-off-by: Edwin Kempin <ekempin@google.com>
765 lines
27 KiB
Java
765 lines
27 KiB
Java
// 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.base.CharMatcher;
|
|
import com.google.common.base.Splitter;
|
|
import com.google.common.flogger.FluentLogger;
|
|
import com.google.gerrit.common.PageLinks;
|
|
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.reviewdb.client.Project;
|
|
import com.google.gerrit.server.AnonymousUser;
|
|
import com.google.gerrit.server.CurrentUser;
|
|
import com.google.gerrit.server.IdentifiedUser;
|
|
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.GitRepositoryManager;
|
|
import com.google.gerrit.server.git.LocalDiskRepositoryManager;
|
|
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.gwtexpui.server.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.Enumeration;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
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.lib.Config;
|
|
import org.eclipse.jgit.lib.Repository;
|
|
|
|
/** Invokes {@code gitweb.cgi} for the project given in {@code p}. */
|
|
@SuppressWarnings("serial")
|
|
@Singleton
|
|
class GitwebServlet extends HttpServlet {
|
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
|
|
|
private static final String PROJECT_LIST_ACTION = "project_list";
|
|
|
|
private final Set<String> deniedActions;
|
|
private final int bufferSize = 8192;
|
|
private final Path gitwebCgi;
|
|
private final URI gitwebUrl;
|
|
private final LocalDiskRepositoryManager repoManager;
|
|
private final ProjectCache projectCache;
|
|
private final PermissionBackend permissionBackend;
|
|
private final Provider<AnonymousUser> anonymousUserProvider;
|
|
private final Provider<CurrentUser> userProvider;
|
|
private final EnvList _env;
|
|
|
|
@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)
|
|
throws IOException {
|
|
if (!(repoManager instanceof LocalDiskRepositoryManager)) {
|
|
throw new ProvisionException("Gitweb can only be used with LocalDiskRepositoryManager");
|
|
}
|
|
this.repoManager = (LocalDiskRepositoryManager) repoManager;
|
|
this.projectCache = projectCache;
|
|
this.permissionBackend = permissionBackend;
|
|
this.anonymousUserProvider = anonymousUserProvider;
|
|
this.userProvider = userProvider;
|
|
this.gitwebCgi = gitwebCgiConfig.getGitwebCgi();
|
|
this.deniedActions = new HashSet<>();
|
|
|
|
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().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.
|
|
//
|
|
// TODO(dborowitz): Is there a portable way to do this with NIO?
|
|
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 = new Project.NameKey(name);
|
|
ProjectState projectState;
|
|
try {
|
|
projectState = projectCache.checkedGet(nameKey);
|
|
if (projectState == null) {
|
|
sendErrorOrRedirect(req, rsp, HttpServletResponse.SC_NOT_FOUND);
|
|
return;
|
|
}
|
|
|
|
projectState.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);
|
|
} 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(), bufferSize)) {
|
|
readCgiHeaders(rsp, in);
|
|
|
|
try (OutputStream out = rsp.getOutputStream()) {
|
|
final byte[] buf = new byte[bufferSize];
|
|
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) {
|
|
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());
|
|
|
|
final Enumeration<String> hdrs = enumerateHeaderNames(req);
|
|
while (hdrs.hasMoreElements()) {
|
|
final String name = hdrs.nextElement();
|
|
final String value = req.getHeader(name);
|
|
env.set("HTTP_" + name.toUpperCase().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", repoManager.getBasePath(nameKey).toAbsolutePath().toString());
|
|
|
|
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();
|
|
}
|
|
|
|
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[bufferSize];
|
|
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(
|
|
() -> {
|
|
StringBuilder b = new StringBuilder();
|
|
try (BufferedReader br =
|
|
new BufferedReader(new InputStreamReader(in, ISO_8859_1.name()))) {
|
|
String line;
|
|
while ((line = br.readLine()) != null) {
|
|
if (b.length() > 0) {
|
|
b.append('\n');
|
|
}
|
|
b.append("CGI: ").append(line);
|
|
}
|
|
logger.atSevere().log(b.toString());
|
|
} catch (IOException e) {
|
|
logger.atSevere().withCause(e).log("Unexpected error copying stderr from CGI");
|
|
}
|
|
},
|
|
"Gitweb-ErrorLogger")
|
|
.start();
|
|
}
|
|
|
|
private static Enumeration<String> enumerateHeaderNames(HttpServletRequest req) {
|
|
return req.getHeaderNames();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
/** 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();
|
|
}
|
|
}
|
|
}
|