| // 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; |
| } |
| } |
| } |