blob: 8252fb29c93de5e437e29588e567517e361360e5 [file] [log] [blame]
// Copyright (C) 2014 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.imagare;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import com.google.common.base.CharMatcher;
import com.google.common.hash.Hashing;
import com.google.common.net.HttpHeaders;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.gerrit.server.project.GetHead;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import eu.medsea.mimeutil.MimeType;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
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.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
@Singleton
public class ImageServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public static final String PATH_PREFIX = "/project/";
private final String pluginName;
private final Provider<ReviewDb> db;
private final ProjectControl.Factory projectControlFactory;
private final ProjectCache projectCache;
private final Provider<GetHead> getHead;
private final GitRepositoryManager repoManager;
private final FileTypeRegistry fileTypeRegistry;
@Inject
ImageServlet(
@PluginName String pluginName,
Provider<ReviewDb> db,
ProjectControl.Factory projectControlFactory,
ProjectCache projectCache,
Provider<GetHead> getHead,
GitRepositoryManager repoManager,
FileTypeRegistry fileTypeRegistry) {
this.pluginName = pluginName;
this.db = db;
this.projectControlFactory = projectControlFactory;
this.projectCache = projectCache;
this.getHead = getHead;
this.repoManager = repoManager;
this.fileTypeRegistry = fileTypeRegistry;
}
@Override
public void service(HttpServletRequest req, HttpServletResponse res) throws IOException {
if (!"GET".equals(req.getMethod()) && !"HEAD".equals(req.getMethod())) {
CacheHeaders.setNotCacheable(res);
res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
return;
}
ResourceKey key = ResourceKey.fromPath(getEncodedPath(req));
ProjectState state = projectCache.get(key.project);
if (state == null || key.file == null) {
notFound(res);
return;
}
MimeType mimeType = fileTypeRegistry.getMimeType(key.file, null);
if (!("image".equals(mimeType.getMediaType()) && fileTypeRegistry.isSafeInline(mimeType))) {
notFound(res);
return;
}
try {
ProjectControl projectControl = projectControlFactory.validateFor(key.project);
String rev = key.revision;
if (rev == null || Constants.HEAD.equals(rev)) {
rev = getHead.get().apply(new ProjectResource(projectControl));
} else {
if (!ObjectId.isId(rev)) {
if (!rev.startsWith(Constants.R_REFS)) {
rev = Constants.R_HEADS + rev;
}
if (!projectControl.controlForRef(rev).isVisible()) {
notFound(res);
return;
}
}
}
try (Repository repo = repoManager.openRepository(key.project)) {
ObjectId revId = repo.resolve(rev != null ? rev : Constants.HEAD);
if (revId == null) {
notFound(res);
return;
}
if (ObjectId.isId(rev)) {
try (RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(repo.resolve(rev));
if (!projectControl.canReadCommit(db.get(), repo, commit)) {
notFound(res);
return;
}
}
}
String eTag = null;
String receivedETag = req.getHeader(HttpHeaders.IF_NONE_MATCH);
if (receivedETag != null) {
eTag = computeETag(key.project, revId, key.file);
if (eTag.equals(receivedETag)) {
res.sendError(SC_NOT_MODIFIED);
return;
}
}
if (!"image".equals(mimeType.getMediaType())) {
notFound(res);
return;
}
try (RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(revId);
RevTree tree = commit.getTree();
try (TreeWalk tw = new TreeWalk(repo)) {
tw.addTree(tree);
tw.setRecursive(true);
tw.setFilter(PathFilter.create(key.file));
if (!tw.next()) {
notFound(res);
return;
}
ObjectId objectId = tw.getObjectId(0);
ObjectLoader loader = repo.open(objectId);
byte[] content = loader.getBytes(Integer.MAX_VALUE);
mimeType = fileTypeRegistry.getMimeType(key.file, content);
if (!"image".equals(mimeType.getMediaType())
|| !fileTypeRegistry.isSafeInline(mimeType)) {
notFound(res);
return;
}
res.setHeader(
HttpHeaders.ETAG, eTag != null ? eTag : computeETag(key.project, revId, key.file));
CacheHeaders.setCacheablePrivate(res, 7, TimeUnit.DAYS, false);
send(req, res, content, mimeType.toString(), commit.getCommitTime());
return;
}
} catch (IOException e) {
notFound(res);
return;
}
}
} catch (RepositoryNotFoundException
| NoSuchProjectException
| ResourceNotFoundException
| AuthException
| RevisionSyntaxException e) {
notFound(res);
return;
}
}
private String getEncodedPath(HttpServletRequest req) {
String path = req.getRequestURI();
String prefix = "/plugins/" + pluginName;
if (path.startsWith(prefix)) {
path = path.substring(prefix.length());
}
return path;
}
@SuppressWarnings("deprecation")
private static String computeETag(Project.NameKey project, ObjectId revId, String file) {
return Hashing.md5()
.newHasher()
.putUnencodedChars(project.get())
.putUnencodedChars(revId.getName())
.putUnencodedChars(file)
.hash()
.toString();
}
private void send(
HttpServletRequest req,
HttpServletResponse res,
byte[] content,
String contentType,
long lastModified)
throws IOException {
if (0 < lastModified) {
long ifModifiedSince = req.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
if (ifModifiedSince > 0 && ifModifiedSince == lastModified) {
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
res.setDateHeader("Last-Modified", lastModified);
}
res.setContentType(contentType);
res.setCharacterEncoding(UTF_8.name());
res.setContentLength(content.length);
res.getOutputStream().write(content);
}
private static void notFound(HttpServletResponse res) throws IOException {
CacheHeaders.setNotCacheable(res);
res.sendError(HttpServletResponse.SC_NOT_FOUND);
}
private static class ResourceKey {
final Project.NameKey project;
final String file;
final String revision;
static ResourceKey fromPath(String path) {
String project;
String file = null;
String revision = null;
if (!path.startsWith(PATH_PREFIX)) {
// should not happen since this servlet is only registered to handle
// paths that start with this prefix
throw new IllegalStateException("path must start with '" + PATH_PREFIX + "'");
}
path = path.substring(PATH_PREFIX.length());
int i = path.indexOf('/');
if (i != -1 && i != path.length() - 1) {
project = IdString.fromUrl(path.substring(0, i)).get();
String rest = path.substring(i + 1);
if (rest.startsWith("rev/")) {
if (rest.length() > 4) {
rest = rest.substring(4);
i = rest.indexOf('/');
if (i != -1 && i != path.length() - 1) {
revision = IdString.fromUrl(rest.substring(0, i)).get();
file = IdString.fromUrl(rest.substring(i + 1)).get();
} else {
revision = IdString.fromUrl(rest).get();
}
}
} else {
file = IdString.fromUrl(rest).get();
}
} else {
project = IdString.fromUrl(CharMatcher.is('/').trimTrailingFrom(path)).get();
}
return new ResourceKey(project, file, revision);
}
private ResourceKey(String p, String f, String r) {
project = new Project.NameKey(p);
file = f;
revision = r;
}
}
}