blob: 028edd6091506cc54d39aaa752052d7c722b15fb [file] [log] [blame]
// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.
package com.google.gitiles.dev;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.gitiles.GitilesServlet.STATIC_PREFIX;
import com.google.common.base.Strings;
import com.google.common.html.types.UncheckedConversions;
import com.google.gitiles.BranchRedirect;
import com.google.gitiles.DebugRenderer;
import com.google.gitiles.GitilesAccess;
import com.google.gitiles.GitilesServlet;
import com.google.gitiles.RepositoryDescription;
import com.google.gitiles.RootedDocServlet;
import com.google.gitiles.doc.HtmlSanitizer;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import javax.servlet.Servlet;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.resource.PathResource;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.RepositoryCache;
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.transport.resolver.RepositoryResolver;
import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class DevServer {
private static final Logger log = LoggerFactory.getLogger(DevServer.class);
private static Config defaultConfig() {
Config cfg = new Config();
String cwd = System.getProperty("user.dir");
cfg.setString("gitiles", null, "basePath", cwd);
cfg.setBoolean("gitiles", null, "exportAll", true);
cfg.setString("gitiles", null, "baseGitUrl", "file://" + cwd + "/");
String networkHostName;
try {
networkHostName = InetAddress.getLocalHost().getCanonicalHostName();
} catch (UnknownHostException e) {
networkHostName = "127.0.0.1";
}
cfg.setString(
"gitiles", null, "siteTitle", String.format("Gitiles - %s:%s", networkHostName, cwd));
cfg.setString("gitiles", null, "canonicalHostName", new File(cwd).getName());
return cfg;
}
private static Path findSourceRoot() throws IOException {
String prop = "com.google.gitiles.sourcePath";
String sourceRoot = System.getProperty(prop);
if (sourceRoot == null) {
throw new NoSuchFileException(
String.format("Must set system property %s to top of source directory", prop));
}
return Paths.get(sourceRoot);
}
private final Path sourceRoot;
private final Config cfg;
private final Server httpd;
DevServer(File cfgFile) throws IOException, ConfigInvalidException {
// Jetty doesn't doesn't allow symlinks, so canonicalize.
sourceRoot = findSourceRoot().toRealPath();
Config cfg = defaultConfig();
if (cfgFile.exists() && cfgFile.isFile()) {
FileBasedConfig fcfg = new FileBasedConfig(cfg, cfgFile, FS.DETECTED);
fcfg.load();
cfg = fcfg;
} else {
log.info("Config file {} not found, using defaults", cfgFile.getPath());
}
this.cfg = cfg;
httpd = new Server(cfg.getInt("gitiles", null, "port", 8080));
httpd.setHandler(handler());
}
void start() throws Exception {
httpd.start();
httpd.join();
}
private Handler handler() throws IOException {
ContextHandlerCollection handlers = new ContextHandlerCollection();
handlers.addHandler(staticHandler());
handlers.addHandler(appHandler());
return handlers;
}
private Handler appHandler() {
DebugRenderer renderer =
new DebugRenderer(
STATIC_PREFIX,
Arrays.asList(cfg.getStringList("gitiles", null, "customTemplates")),
sourceRoot.resolve("resources/com/google/gitiles/templates").toString(),
firstNonNull(cfg.getString("gitiles", null, "siteTitle"), "Gitiles"));
String docRoot = cfg.getString("gitiles", null, "docroot");
Servlet servlet;
if (!Strings.isNullOrEmpty(docRoot)) {
servlet = createRootedDocServlet(renderer, docRoot);
} else {
servlet =
new GitilesServlet(
cfg, renderer, null, null, null, null, null, null, null, new BranchRedirect());
}
ServletContextHandler handler = new ServletContextHandler();
handler.setContextPath("");
handler.addServlet(new ServletHolder(servlet), "/*");
return handler;
}
private Handler staticHandler() throws IOException {
Path staticRoot = sourceRoot.resolve("resources/com/google/gitiles/static");
ResourceHandler rh = new ResourceHandler();
try {
rh.setBaseResource(new PathResource(staticRoot.toUri().toURL()));
} catch (URISyntaxException e) {
throw new IOException(e);
}
rh.setWelcomeFiles(new String[] {});
rh.setDirectoriesListed(false);
ContextHandler handler = new ContextHandler("/+static");
handler.setHandler(rh);
return handler;
}
private Servlet createRootedDocServlet(DebugRenderer renderer, String docRoot) {
File docRepo = new File(docRoot);
FileKey repoKey = FileKey.exact(docRepo, FS.DETECTED);
RepositoryResolver<HttpServletRequest> resolver =
(req, name) -> {
try {
return RepositoryCache.open(repoKey, true);
} catch (IOException e) {
throw new RepositoryNotFoundException(repoKey.getFile(), e);
}
};
HtmlSanitizer.Factory htmlSanitizer = HtmlSanitizer.DISABLED_FACTORY;
if (cfg.getBoolean("markdown", "unsafeAllowUserContentHtmlInDevMode", false)) {
log.warn("!!! Allowing unsafe user content HTML in Markdown !!!");
htmlSanitizer =
request ->
rawUnsafeHtml ->
// Yes, this is evil. It's not known the input was safe.
// I'm a development server to test Gitiles, not a cop.
UncheckedConversions.safeHtmlFromStringKnownToSatisfyTypeContract(rawUnsafeHtml);
}
return new RootedDocServlet(resolver, new RootedDocAccess(docRepo), renderer, htmlSanitizer);
}
private class RootedDocAccess implements GitilesAccess.Factory {
private final String repoName;
RootedDocAccess(File docRepo) {
if (Constants.DOT_GIT.equals(docRepo.getName())) {
repoName = docRepo.getParentFile().getName();
} else {
repoName = docRepo.getName();
}
}
@Override
public GitilesAccess forRequest(HttpServletRequest req) {
return new GitilesAccess() {
@Override
public Map<String, RepositoryDescription> listRepositories(
String prefix, Set<String> branches) {
return Collections.emptyMap();
}
@Override
public Object getUserKey() {
return null;
}
@Override
public String getRepositoryName() {
return repoName;
}
@Override
public RepositoryDescription getRepositoryDescription() {
RepositoryDescription d = new RepositoryDescription();
d.name = getRepositoryName();
return d;
}
@Override
public Config getConfig() {
return cfg;
}
};
}
}
}