Line data Source code
1 : // Copyright (C) 2015 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 : package com.google.gerrit.httpd.raw;
16 :
17 : import static java.nio.file.Files.exists;
18 : import static java.nio.file.Files.isReadable;
19 :
20 : import com.google.common.cache.Cache;
21 : import com.google.common.collect.ImmutableList;
22 : import com.google.common.flogger.FluentLogger;
23 : import com.google.gerrit.common.Nullable;
24 : import com.google.gerrit.extensions.api.GerritApi;
25 : import com.google.gerrit.httpd.XsrfCookieFilter;
26 : import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
27 : import com.google.gerrit.launcher.GerritLauncher;
28 : import com.google.gerrit.server.cache.CacheModule;
29 : import com.google.gerrit.server.config.CanonicalWebUrl;
30 : import com.google.gerrit.server.config.GerritOptions;
31 : import com.google.gerrit.server.config.GerritServerConfig;
32 : import com.google.gerrit.server.config.SitePaths;
33 : import com.google.gerrit.server.experiments.ExperimentFeatures;
34 : import com.google.inject.Inject;
35 : import com.google.inject.Key;
36 : import com.google.inject.Provides;
37 : import com.google.inject.ProvisionException;
38 : import com.google.inject.Singleton;
39 : import com.google.inject.name.Named;
40 : import com.google.inject.name.Names;
41 : import com.google.inject.servlet.ServletModule;
42 : import java.io.File;
43 : import java.io.FileNotFoundException;
44 : import java.io.IOException;
45 : import java.nio.file.FileSystem;
46 : import java.nio.file.Path;
47 : import javax.servlet.Filter;
48 : import javax.servlet.FilterChain;
49 : import javax.servlet.FilterConfig;
50 : import javax.servlet.ServletException;
51 : import javax.servlet.ServletRequest;
52 : import javax.servlet.ServletResponse;
53 : import javax.servlet.http.HttpServlet;
54 : import javax.servlet.http.HttpServletRequest;
55 : import javax.servlet.http.HttpServletRequestWrapper;
56 : import javax.servlet.http.HttpServletResponse;
57 : import org.eclipse.jgit.http.server.GitSmartHttpTools;
58 : import org.eclipse.jgit.lib.Config;
59 :
60 : public class StaticModule extends ServletModule {
61 99 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
62 :
63 : public static final String CACHE = "static_content";
64 :
65 : /**
66 : * Paths at which we should serve the main PolyGerrit application {@code index.html}.
67 : *
68 : * <p>Supports {@code "/*"} as a trailing wildcard.
69 : */
70 99 : public static final ImmutableList<String> POLYGERRIT_INDEX_PATHS =
71 99 : ImmutableList.of(
72 : "/",
73 : "/c/*",
74 : "/id/*",
75 : "/p/*",
76 : "/q/*",
77 : "/x/*",
78 : "/admin/*",
79 : "/dashboard/*",
80 : "/groups/self",
81 : "/settings/*",
82 : "/topic/*",
83 : "/Documentation/q/*");
84 :
85 : /**
86 : * Paths that should be treated as static assets when serving PolyGerrit.
87 : *
88 : * <p>Supports {@code "/*"} as a trailing wildcard.
89 : */
90 99 : private static final ImmutableList<String> POLYGERRIT_ASSET_PATHS =
91 99 : ImmutableList.of(
92 : "/behaviors/*",
93 : "/bower_components/*",
94 : "/elements/*",
95 : "/fonts/*",
96 : "/scripts/*",
97 : "/styles/*",
98 : "/workers/*");
99 :
100 : private static final String DOC_SERVLET = "DocServlet";
101 : private static final String FAVICON_SERVLET = "FaviconServlet";
102 : private static final String SERVICE_WORKER_SERVLET = "ServiceWorkerServlet";
103 : private static final String POLYGERRIT_INDEX_SERVLET = "PolyGerritUiIndexServlet";
104 : private static final String ROBOTS_TXT_SERVLET = "RobotsTxtServlet";
105 :
106 : private final GerritOptions options;
107 : private Paths paths;
108 :
109 : @Inject
110 99 : public StaticModule(GerritOptions options) {
111 99 : this.options = options;
112 99 : }
113 :
114 : @Provides
115 : @Singleton
116 : private Paths getPaths() {
117 99 : if (paths == null) {
118 99 : paths = new Paths(options);
119 : }
120 99 : return paths;
121 : }
122 :
123 : @Override
124 : protected void configureServlets() {
125 99 : serveRegex("^/Documentation$").with(named(DOC_SERVLET));
126 99 : serveRegex("^/Documentation/$").with(named(DOC_SERVLET));
127 99 : serveRegex("^/Documentation/(.+)$").with(named(DOC_SERVLET));
128 99 : serve("/static/*").with(SiteStaticDirectoryServlet.class);
129 99 : install(
130 99 : new CacheModule() {
131 : @Override
132 : protected void configure() {
133 99 : cache(CACHE, Path.class, Resource.class)
134 99 : .maximumWeight(1 << 20)
135 99 : .weigher(ResourceServlet.Weigher.class);
136 99 : }
137 : });
138 99 : if (!options.headless()) {
139 0 : install(new CoreStaticModule());
140 0 : install(new PolyGerritModule());
141 : }
142 99 : }
143 :
144 : @Provides
145 : @Singleton
146 : @Named(DOC_SERVLET)
147 : HttpServlet getDocServlet(
148 : @Named(CACHE) Cache<Path, Resource> cache, ExperimentFeatures experimentFeatures) {
149 99 : Paths p = getPaths();
150 99 : if (p.warFs != null) {
151 0 : return new WarDocServlet(cache, p.warFs, experimentFeatures);
152 99 : } else if (p.unpackedWar != null && !p.isDev()) {
153 99 : return new DirectoryDocServlet(cache, p.unpackedWar, experimentFeatures);
154 : } else {
155 0 : return new HttpServlet() {
156 : private static final long serialVersionUID = 1L;
157 :
158 : @Override
159 : protected void service(HttpServletRequest req, HttpServletResponse resp)
160 : throws IOException {
161 0 : resp.sendError(HttpServletResponse.SC_NOT_FOUND);
162 0 : }
163 : };
164 : }
165 : }
166 :
167 0 : private class CoreStaticModule extends ServletModule {
168 : @Override
169 : public void configureServlets() {
170 0 : serve("/robots.txt").with(named(ROBOTS_TXT_SERVLET));
171 0 : serve("/favicon.ico").with(named(FAVICON_SERVLET));
172 0 : serve("/service-worker.js").with(named(SERVICE_WORKER_SERVLET));
173 0 : }
174 :
175 : @Provides
176 : @Singleton
177 : @Named(ROBOTS_TXT_SERVLET)
178 : HttpServlet getRobotsTxtServlet(
179 : @GerritServerConfig Config cfg,
180 : SitePaths sitePaths,
181 : @Named(CACHE) Cache<Path, Resource> cache) {
182 0 : Path configPath = sitePaths.resolve(cfg.getString("httpd", null, "robotsFile"));
183 0 : if (configPath != null) {
184 0 : if (exists(configPath) && isReadable(configPath)) {
185 0 : return new SingleFileServlet(cache, configPath, true);
186 : }
187 0 : logger.atWarning().log("Cannot read httpd.robotsFile, using default");
188 : }
189 0 : Paths p = getPaths();
190 0 : if (p.warFs != null) {
191 0 : return new SingleFileServlet(cache, p.warFs.getPath("/robots.txt"), false);
192 : }
193 0 : return new SingleFileServlet(cache, webappSourcePath("robots.txt"), true);
194 : }
195 :
196 : @Provides
197 : @Singleton
198 : @Named(FAVICON_SERVLET)
199 : HttpServlet getFaviconServlet(@Named(CACHE) Cache<Path, Resource> cache) {
200 0 : Paths p = getPaths();
201 0 : if (p.warFs != null) {
202 0 : return new SingleFileServlet(cache, p.warFs.getPath("/favicon.ico"), false);
203 : }
204 0 : return new SingleFileServlet(cache, webappSourcePath("favicon.ico"), true);
205 : }
206 :
207 : @Provides
208 : @Singleton
209 : @Named(SERVICE_WORKER_SERVLET)
210 : HttpServlet getServiceWorkerServlet(@Named(CACHE) Cache<Path, Resource> cache) {
211 0 : Paths p = getPaths();
212 0 : if (p.warFs != null) {
213 0 : return new SingleFileServlet(
214 0 : cache, p.warFs.getPath("/polygerrit_ui/workers/service-worker.js"), false);
215 : }
216 0 : return new SingleFileServlet(
217 0 : cache, webappSourcePath("polygerrit_ui/workers/service-worker.js"), true);
218 : }
219 :
220 : private Path webappSourcePath(String name) {
221 0 : Paths p = getPaths();
222 0 : if (p.unpackedWar != null) {
223 0 : return p.unpackedWar.resolve(name);
224 : }
225 0 : return p.sourceRoot.resolve("webapp/" + name);
226 : }
227 : }
228 :
229 0 : private class PolyGerritModule extends ServletModule {
230 : @Override
231 : public void configureServlets() {
232 0 : for (String p : POLYGERRIT_INDEX_PATHS) {
233 0 : filter(p).through(XsrfCookieFilter.class);
234 0 : }
235 0 : filter("/*").through(PolyGerritFilter.class);
236 0 : }
237 :
238 : @Provides
239 : @Singleton
240 : @Named(POLYGERRIT_INDEX_SERVLET)
241 : HttpServlet getPolyGerritUiIndexServlet(
242 : @CanonicalWebUrl @Nullable String canonicalUrl,
243 : @GerritServerConfig Config cfg,
244 : GerritApi gerritApi,
245 : ExperimentFeatures experimentFeatures) {
246 0 : String cdnPath = options.devCdn().orElse(cfg.getString("gerrit", null, "cdnPath"));
247 0 : String faviconPath = cfg.getString("gerrit", null, "faviconPath");
248 0 : return new IndexServlet(canonicalUrl, cdnPath, faviconPath, gerritApi, experimentFeatures);
249 : }
250 :
251 : @Provides
252 : @Singleton
253 : PolyGerritUiServlet getPolyGerritUiServlet(@Named(CACHE) Cache<Path, Resource> cache) {
254 0 : return new PolyGerritUiServlet(cache, polyGerritBasePath());
255 : }
256 :
257 : private Path polyGerritBasePath() {
258 0 : Paths p = getPaths();
259 :
260 0 : return p.warFs != null
261 0 : ? p.warFs.getPath("/polygerrit_ui")
262 0 : : p.unpackedWar.resolve("polygerrit_ui");
263 : }
264 : }
265 :
266 : private static class Paths {
267 : private final FileSystem warFs;
268 : private final Path sourceRoot;
269 : private final Path unpackedWar;
270 : private final boolean development;
271 :
272 99 : private Paths(GerritOptions options) {
273 : try {
274 99 : File launcherLoadedFrom = getLauncherLoadedFrom();
275 99 : if (launcherLoadedFrom != null && launcherLoadedFrom.getName().endsWith(".jar")) {
276 : // Special case: unpacked war archive deployed in container.
277 : // The path is something like:
278 : // <container>/<gerrit>/WEB-INF/lib/launcher.jar
279 : // Switch to exploded war case with <container>/webapp>/<gerrit>
280 : // root directory
281 99 : warFs = null;
282 99 : unpackedWar =
283 99 : java.nio.file.Paths.get(
284 99 : launcherLoadedFrom.getParentFile().getParentFile().getParentFile().toURI());
285 99 : sourceRoot = null;
286 99 : development = false;
287 99 : return;
288 : }
289 0 : warFs = getDistributionArchive(launcherLoadedFrom);
290 0 : if (warFs == null) {
291 0 : unpackedWar = makeWarTempDir();
292 0 : development = true;
293 0 : } else if (options.devCdn().isPresent()) {
294 0 : unpackedWar = null;
295 0 : development = true;
296 : } else {
297 0 : unpackedWar = null;
298 0 : development = false;
299 0 : sourceRoot = null;
300 0 : return;
301 : }
302 0 : } catch (IOException e) {
303 0 : throw new ProvisionException("Error initializing static content paths", e);
304 0 : }
305 :
306 0 : sourceRoot = getSourceRootOrNull();
307 0 : }
308 :
309 : @Nullable
310 : private static Path getSourceRootOrNull() {
311 : try {
312 0 : return GerritLauncher.resolveInSourceRoot(".");
313 0 : } catch (FileNotFoundException e) {
314 0 : return null;
315 : }
316 : }
317 :
318 : @Nullable
319 : private FileSystem getDistributionArchive(File war) throws IOException {
320 0 : if (war == null) {
321 0 : return null;
322 : }
323 0 : return GerritLauncher.getZipFileSystem(war.toPath());
324 : }
325 :
326 : @Nullable
327 : private File getLauncherLoadedFrom() {
328 : File war;
329 : try {
330 99 : war = GerritLauncher.getDistributionArchive();
331 0 : } catch (IOException e) {
332 0 : if ((e instanceof FileNotFoundException)
333 0 : && GerritLauncher.NOT_ARCHIVED.equals(e.getMessage())) {
334 0 : return null;
335 : }
336 0 : throw new ProvisionException("Error reading gerrit.war", e);
337 99 : }
338 99 : return war;
339 : }
340 :
341 : private boolean isDev() {
342 99 : return development;
343 : }
344 :
345 : private Path makeWarTempDir() {
346 : // Obtain our local temporary directory, but it comes back as a file
347 : // so we have to switch it to be a directory post creation.
348 : //
349 : try {
350 0 : File dstwar = GerritLauncher.createTempFile("gerrit_", "war");
351 0 : if (!dstwar.delete() || !dstwar.mkdir()) {
352 0 : throw new IOException("Cannot mkdir " + dstwar.getAbsolutePath());
353 : }
354 :
355 : // Jetty normally refuses to serve out of a symlinked directory, as
356 : // a security feature. Try to resolve out any symlinks in the path.
357 : //
358 : try {
359 0 : return dstwar.getCanonicalFile().toPath();
360 0 : } catch (IOException e) {
361 0 : return dstwar.getAbsoluteFile().toPath();
362 : }
363 0 : } catch (IOException e) {
364 0 : throw new ProvisionException("Cannot create war tempdir", e);
365 : }
366 : }
367 : }
368 :
369 : private static Key<HttpServlet> named(String name) {
370 99 : return Key.get(HttpServlet.class, Names.named(name));
371 : }
372 :
373 : @Singleton
374 : private static class PolyGerritFilter implements Filter {
375 : private final HttpServlet polyGerritIndex;
376 : private final PolyGerritUiServlet polygerritUI;
377 :
378 : @Inject
379 : PolyGerritFilter(
380 : @Named(POLYGERRIT_INDEX_SERVLET) HttpServlet polyGerritIndex,
381 0 : PolyGerritUiServlet polygerritUI) {
382 0 : this.polyGerritIndex = polyGerritIndex;
383 0 : this.polygerritUI = polygerritUI;
384 0 : }
385 :
386 : @Override
387 0 : public void init(FilterConfig filterConfig) throws ServletException {}
388 :
389 : @Override
390 0 : public void destroy() {}
391 :
392 : @Override
393 : public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
394 : throws IOException, ServletException {
395 0 : HttpServletRequest req = (HttpServletRequest) request;
396 0 : HttpServletResponse res = (HttpServletResponse) response;
397 :
398 0 : if (!GitSmartHttpTools.isGitClient(req)) {
399 0 : GuiceFilterRequestWrapper reqWrapper = new GuiceFilterRequestWrapper(req);
400 0 : String path = pathInfo(req);
401 :
402 0 : if (isPolyGerritIndex(path)) {
403 0 : polyGerritIndex.service(reqWrapper, res);
404 0 : return;
405 : }
406 0 : if (isPolyGerritAsset(path)) {
407 0 : polygerritUI.service(reqWrapper, res);
408 0 : return;
409 : }
410 : }
411 :
412 0 : chain.doFilter(req, res);
413 0 : }
414 :
415 : private static String pathInfo(HttpServletRequest req) {
416 0 : String uri = req.getRequestURI();
417 0 : String ctx = req.getContextPath();
418 0 : return uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri;
419 : }
420 :
421 : private static boolean isPolyGerritAsset(String path) {
422 0 : return matchPath(POLYGERRIT_ASSET_PATHS, path);
423 : }
424 :
425 : private static boolean isPolyGerritIndex(String path) {
426 0 : return matchPath(POLYGERRIT_INDEX_PATHS, path);
427 : }
428 :
429 : private static boolean matchPath(Iterable<String> paths, String path) {
430 0 : for (String p : paths) {
431 0 : if (p.endsWith("/*")) {
432 0 : if (path.regionMatches(0, p, 0, p.length() - 1)) {
433 0 : return true;
434 : }
435 0 : } else if (p.equals(path)) {
436 0 : return true;
437 : }
438 0 : }
439 0 : return false;
440 : }
441 : }
442 :
443 : private static class GuiceFilterRequestWrapper extends HttpServletRequestWrapper {
444 : GuiceFilterRequestWrapper(HttpServletRequest req) {
445 0 : super(req);
446 0 : }
447 :
448 : @Nullable
449 : @Override
450 : public String getPathInfo() {
451 0 : String uri = getRequestURI();
452 0 : String ctx = getContextPath();
453 : // This is a workaround for long standing guice filter bug:
454 : // https://github.com/google/guice/issues/807
455 0 : String res = uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri;
456 :
457 : // Match the logic in the ResourceServlet, that re-add "/"
458 : // for null path info
459 0 : if ("/".equals(res)) {
460 0 : return null;
461 : }
462 0 : return res;
463 : }
464 : }
465 : }
|