blob: 01760bde5036cc64426f88f068d95bcdd5996f84 [file] [log] [blame]
// Copyright (C) 2013 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.
package com.googlesource.gerrit.plugins.serverconfig;
import com.google.common.base.CharMatcher;
import com.google.common.base.Charsets;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.io.ByteStreams;
import com.google.gerrit.audit.AuditEvent;
import com.google.gerrit.audit.AuditService;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.SitePaths;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Singleton
public class ServerConfigServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger log = LoggerFactory
.getLogger(ServerConfigServlet.class);
private final SitePaths sitePaths;
private final AuditService auditService;
private final DynamicItem<WebSession> webSession;
private final String pluginName;
@Inject
ServerConfigServlet(SitePaths sitePaths, DynamicItem<WebSession> webSession,
AuditService auditService, @PluginName String pluginName) {
this.webSession = webSession;
this.auditService = auditService;
this.pluginName = pluginName;
this.sitePaths = sitePaths;
}
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException {
if (!isAllowedPath(req)) {
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
streamFile(req, res);
}
@Override
public void doPut(HttpServletRequest req, HttpServletResponse res)
throws IOException {
if (!isAllowedPath(req)) {
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
if (isGerritConfig(req)) {
writeFileAndFireAuditEvent(req, res);
} else {
writeFile(req, res);
}
}
private void writeFileAndFireAuditEvent(HttpServletRequest req,
HttpServletResponse res) throws IOException {
File oldFile = resolvePath(req).toFile();
File dir = oldFile.getParentFile();
File newFile = File.createTempFile(oldFile.getName(), ".new", dir);
streamRequestToFile(req, newFile);
try {
FileBasedConfig config = new FileBasedConfig(newFile, FS.DETECTED);
config.load();
String diff = diff(oldFile, newFile);
audit("about to change config file", oldFile.getPath(), diff);
newFile.renameTo(oldFile);
audit("changed config file", oldFile.getPath(), diff);
res.setStatus(HttpServletResponse.SC_NO_CONTENT);
} catch (ConfigInvalidException e) {
log.warn("Configuration file is invalid", e);
Throwable cause = e.getCause();
final String msg =
cause instanceof ConfigInvalidException ? cause.getMessage()
: e.getMessage();
newFile.delete();
respondInvalidConfig(req, res, msg);
}
}
private void respondInvalidConfig(HttpServletRequest req,
HttpServletResponse res, String messageTxt) throws IOException {
String message =
MessageFormat.format("Invalid config file {0}: {1}", req.getPathInfo(),
messageTxt);
res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
res.setContentType("application/octet-stream");
res.setContentLength(message.length());
byte[] bytes = message.getBytes(Charsets.UTF_8);
OutputStream out = res.getOutputStream();
try (ByteArrayInputStream in = new ByteArrayInputStream(bytes)) {
ByteStreams.copy(in, out);
}
}
private static String diff(File oldFile, File newFile) throws IOException {
RawText oldContext = new RawText(oldFile);
RawText newContext = new RawText(newFile);
UnifiedDiffer differ = new UnifiedDiffer();
return differ.diff(oldContext, newContext);
}
private void audit(String what, String path, String diff) {
String sessionId = webSession.get().getSessionId();
CurrentUser who = webSession.get().getUser();
long when = TimeUtil.nowMs();
ListMultimap<String, Object> params =
MultimapBuilder.hashKeys().arrayListValues().build();
params.put("plugin", pluginName);
params.put("class", ServerConfigServlet.class);
params.put("diff", diff);
params.put("file", path);
auditService.dispatch(new AuditEvent(sessionId, who, what, when, params, null));
}
private boolean isGerritConfig(HttpServletRequest req) throws IOException {
return Files.isSameFile(sitePaths.gerrit_config, resolvePath(req));
}
private boolean isAllowedPath(HttpServletRequest req) throws IOException {
Path p = resolvePath(req);
if (!Files.isRegularFile(p)) {
return false;
}
return isParent(sitePaths.etc_dir, p) || isParent(sitePaths.static_dir, p);
}
private Path resolvePath(HttpServletRequest req) {
return sitePaths.resolve(CharMatcher.is('/').trimLeadingFrom(
req.getServletPath() + req.getPathInfo()));
}
private boolean isParent(Path parent, Path child) throws IOException {
Path p = child;
for (;;) {
p = p.getParent();
if (p == null) {
return false;
}
if (Files.isSameFile(p, parent)) {
return true;
}
}
}
private void streamFile(HttpServletRequest req, HttpServletResponse res)
throws IOException {
File f = resolvePath(req).toFile();
res.setStatus(HttpServletResponse.SC_OK);
res.setContentType("application/octet-stream");
res.setContentLength((int) f.length());
OutputStream out = res.getOutputStream();
try (InputStream in = new FileInputStream(f)) {
ByteStreams.copy(in, out);
}
}
private void writeFile(HttpServletRequest req, HttpServletResponse res)
throws IOException {
res.setStatus(HttpServletResponse.SC_NO_CONTENT);
streamRequestToFile(req, resolvePath(req).toFile());
}
private void streamRequestToFile(HttpServletRequest req, File file)
throws IOException, FileNotFoundException {
InputStream in = req.getInputStream();
try (OutputStream out = new FileOutputStream(file)) {
ByteStreams.copy(in, out);
}
}
}