blob: f1e01bcff415c92475030840bb7ce8f9d53b0fe2 [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;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.net.HttpHeaders.LOCATION;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_GONE;
import static javax.servlet.http.HttpServletResponse.SC_MOVED_PERMANENTLY;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.gitiles.GitilesView.InvalidViewException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.List;
import java.util.regex.Pattern;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jgit.lib.ObjectId;
/** Filter to redirect Gitweb-style URLs to Gitiles-style URLs. */
public class GitwebRedirectFilter extends AbstractHttpFilter {
public static class TooManyUriParametersException extends IllegalArgumentException {
private static final long serialVersionUID = 1L;
}
private static final String ARG_SEP = "&|;|%3[Bb]";
private static final Pattern IS_GITWEB_PATTERN = Pattern.compile("(^|" + ARG_SEP + ")[pa]=");
private static final Splitter ARG_SPLIT = Splitter.on(Pattern.compile(ARG_SEP));
private static final Splitter VAR_SPLIT = Splitter.on(Pattern.compile("=|%3[Dd]")).limit(2);
private static final int MAX_ARGS = 512;
private final boolean trimDotGit;
public GitwebRedirectFilter() {
this(false);
}
public GitwebRedirectFilter(boolean trimDotGit) {
this.trimDotGit = trimDotGit;
}
@Override
public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
GitilesView gitwebView = ViewFilter.getView(req);
if (!isGitwebStyleQuery(req)) {
chain.doFilter(req, res);
return;
}
ListMultimap<String, String> params = parse(req.getQueryString());
String action = getFirst(params, "a");
String project = getFirst(params, "p");
String path = Strings.nullToEmpty(getFirst(params, "f"));
// According to gitweb's perl source code, the primary parameters are these
// short abbreviated names. When pointing to blob or subtree hash,hashParent
// are the blob or subtree SHA-1s and hashBase,hashParentBase are commits.
// When pointing to commits or tags, hash is the commit/tag. Its messy.
Revision hash = toRevision(getFirst(params, "h"));
Revision hashBase = toRevision(getFirst(params, "hb"));
Revision hashParent = toRevision(getFirst(params, "hp"));
Revision hashParentBase = toRevision(getFirst(params, "hpb"));
GitilesView.Builder view;
if ("project_index".equals(action)) {
view = GitilesView.hostIndex();
project = null;
} else if ("summary".equals(action) || "tags".equals(action)) {
view = GitilesView.repositoryIndex();
} else if (("commit".equals(action) || "tag".equals(action)) && hash != null) {
view = GitilesView.revision().setRevision(hash);
} else if ("log".equals(action) || "shortlog".equals(action)) {
view = GitilesView.log().setRevision(firstNonNull(hash, Revision.HEAD));
} else if ("tree".equals(action)) {
view =
GitilesView.path().setRevision(firstNonNull(hashBase, Revision.HEAD)).setPathPart(path);
} else if (("blob".equals(action) || "blob_plain".equals(action))
&& hashBase != null
&& !path.isEmpty()) {
view = GitilesView.path().setRevision(hashBase).setPathPart(path);
} else if ("commitdiff".equals(action) && hash != null) {
view =
GitilesView.diff()
.setOldRevision(firstNonNull(hashParent, Revision.NULL))
.setRevision(hash)
.setPathPart("");
} else if ("blobdiff".equals(action)
&& !path.isEmpty()
&& hashParentBase != null
&& hashBase != null) {
view =
GitilesView.diff().setOldRevision(hashParentBase).setRevision(hashBase).setPathPart(path);
} else if ("history".equals(action) && !path.isEmpty()) {
view = GitilesView.log().setRevision(firstNonNull(hashBase, Revision.HEAD)).setPathPart(path);
} else {
// Gitiles does not provide an RSS feed (a=rss,atom,opml)
// Any other URL is out of date and not valid anymore.
res.sendError(SC_GONE);
return;
}
if (!Strings.isNullOrEmpty(project)) {
view.setRepositoryName(cleanProjectName(project));
}
String url;
try {
url =
view.setHostName(gitwebView.getHostName())
.setServletPath(gitwebView.getServletPath())
.toUrl();
} catch (InvalidViewException e) {
res.setStatus(SC_GONE);
return;
}
res.setStatus(SC_MOVED_PERMANENTLY);
res.setHeader(LOCATION, url);
}
private static boolean isGitwebStyleQuery(HttpServletRequest req) {
String qs = req.getQueryString();
return qs != null && IS_GITWEB_PATTERN.matcher(qs).find();
}
private static String getFirst(ListMultimap<String, String> params, String name) {
return Iterables.getFirst(params.get(checkNotNull(name)), null);
}
private static Revision toRevision(String rev) {
if (Strings.isNullOrEmpty(rev)) {
return null;
} else if ("HEAD".equals(rev) || rev.startsWith("refs/")) {
return Revision.named(rev);
} else if (ObjectId.isId(rev)) {
return Revision.unpeeled(rev, ObjectId.fromString(rev));
} else {
return Revision.named(rev);
}
}
private String cleanProjectName(String p) {
if (p.startsWith("/")) {
p = p.substring(1);
}
if (p.endsWith("/")) {
p = p.substring(0, p.length() - 1);
}
if (trimDotGit && p.endsWith(".git")) {
p = p.substring(0, p.length() - ".git".length());
}
if (p.endsWith("/")) {
p = p.substring(0, p.length() - 1);
}
return p;
}
private static ListMultimap<String, String> parse(String query) {
// Parse a gitweb style query string which uses ";" rather than "&" between
// key=value pairs. Some user agents encode ";" as "%3B" and/or "=" as
// "%3D", making a real mess of the query string. Parsing here is
// approximate because ; shouldn't be the pair separator and %3B might have
// been a ; within a value.
// This is why people shouldn't use gitweb.
ListMultimap<String, String> map = LinkedListMultimap.create();
for (String piece : ARG_SPLIT.split(query)) {
if (map.size() > MAX_ARGS) {
throw new TooManyUriParametersException();
}
List<String> pair = VAR_SPLIT.splitToList(piece);
if (pair.size() == 2) {
map.put(decode(pair.get(0)), decode(pair.get(1)));
} else { // no equals sign
map.put(piece, "");
}
}
return map;
}
private static String decode(String str) {
try {
return URLDecoder.decode(str, UTF_8.name());
} catch (UnsupportedEncodingException e) {
return str;
}
}
}