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 : }
|