| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 1 | // Copyright 2012 Google Inc. All Rights Reserved. |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package com.google.gitiles; |
| 16 | |
| 17 | import static com.google.common.base.Preconditions.checkNotNull; |
| 18 | |
| Dave Borowitz | fa0fef0 | 2013-01-07 13:17:53 -0800 | [diff] [blame] | 19 | import com.google.common.base.CharMatcher; |
| Shawn Pearce | c709c4c | 2015-08-28 15:30:42 -0700 | [diff] [blame] | 20 | import com.google.common.base.Strings; |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 21 | import com.google.common.collect.Lists; |
| 22 | import com.google.common.collect.Maps; |
| 23 | import com.google.common.collect.Queues; |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 24 | import java.io.File; |
| 25 | import java.io.IOException; |
| Dave Borowitz | 9e95e63 | 2013-11-27 16:16:24 -0500 | [diff] [blame] | 26 | import java.text.Collator; |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 27 | import java.util.Collection; |
| 28 | import java.util.Collections; |
| 29 | import java.util.List; |
| Dave Borowitz | 9e95e63 | 2013-11-27 16:16:24 -0500 | [diff] [blame] | 30 | import java.util.Locale; |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 31 | import java.util.Map; |
| 32 | import java.util.Queue; |
| 33 | import java.util.Set; |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 34 | import javax.servlet.http.HttpServletRequest; |
| Dave Borowitz | 3b744b1 | 2016-08-19 16:11:10 -0400 | [diff] [blame] | 35 | import org.eclipse.jgit.errors.ConfigInvalidException; |
| 36 | import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| 37 | import org.eclipse.jgit.http.server.ServletUtils; |
| 38 | import org.eclipse.jgit.lib.Config; |
| 39 | import org.eclipse.jgit.lib.Ref; |
| 40 | import org.eclipse.jgit.lib.Repository; |
| 41 | import org.eclipse.jgit.lib.StoredConfig; |
| 42 | import org.eclipse.jgit.transport.resolver.FileResolver; |
| 43 | import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; |
| 44 | import org.eclipse.jgit.util.IO; |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 45 | |
| 46 | /** |
| 47 | * Default implementation of {@link GitilesAccess} with local repositories. |
| Dave Borowitz | 40255d5 | 2016-08-19 16:16:22 -0400 | [diff] [blame] | 48 | * |
| 49 | * <p>Repositories are scanned on-demand under the given path, configured by default from {@code |
| 50 | * gitiles.basePath}. There is no access control beyond what user the JVM is running under. |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 51 | */ |
| 52 | public class DefaultAccess implements GitilesAccess { |
| 53 | private static final String ANONYMOUS_USER_KEY = "anonymous user"; |
| 54 | |
| Dave Borowitz | fa0fef0 | 2013-01-07 13:17:53 -0800 | [diff] [blame] | 55 | private static final String DEFAULT_DESCRIPTION = |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 56 | "Unnamed repository; edit this file 'description' to name the repository."; |
| Dave Borowitz | fa0fef0 | 2013-01-07 13:17:53 -0800 | [diff] [blame] | 57 | |
| Dave Borowitz | 9e95e63 | 2013-11-27 16:16:24 -0500 | [diff] [blame] | 58 | private static final Collator US_COLLATOR = Collator.getInstance(Locale.US); |
| 59 | |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 60 | public static class Factory implements GitilesAccess.Factory { |
| 61 | private final File basePath; |
| 62 | private final String canonicalBasePath; |
| 63 | private final String baseGitUrl; |
| Dave Borowitz | ded109a | 2014-03-03 15:25:39 -0500 | [diff] [blame] | 64 | private final Config baseConfig; |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 65 | private final FileResolver<HttpServletRequest> resolver; |
| 66 | |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 67 | Factory( |
| 68 | File basePath, |
| 69 | String baseGitUrl, |
| 70 | Config baseConfig, |
| 71 | FileResolver<HttpServletRequest> resolver) |
| 72 | throws IOException { |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 73 | this.basePath = checkNotNull(basePath, "basePath"); |
| 74 | this.baseGitUrl = checkNotNull(baseGitUrl, "baseGitUrl"); |
| Dave Borowitz | ded109a | 2014-03-03 15:25:39 -0500 | [diff] [blame] | 75 | this.baseConfig = checkNotNull(baseConfig, "baseConfig"); |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 76 | this.resolver = checkNotNull(resolver, "resolver"); |
| 77 | this.canonicalBasePath = basePath.getCanonicalPath(); |
| 78 | } |
| 79 | |
| 80 | @Override |
| 81 | public GitilesAccess forRequest(HttpServletRequest req) { |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 82 | return newAccess(basePath, canonicalBasePath, baseGitUrl, resolver, req); |
| 83 | } |
| 84 | |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 85 | protected DefaultAccess newAccess( |
| 86 | File basePath, |
| 87 | String canonicalBasePath, |
| 88 | String baseGitUrl, |
| 89 | FileResolver<HttpServletRequest> resolver, |
| 90 | HttpServletRequest req) { |
| Dave Borowitz | ded109a | 2014-03-03 15:25:39 -0500 | [diff] [blame] | 91 | return new DefaultAccess(basePath, canonicalBasePath, baseGitUrl, baseConfig, resolver, req); |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 92 | } |
| 93 | } |
| 94 | |
| 95 | protected final File basePath; |
| 96 | protected final String canonicalBasePath; |
| 97 | protected final String baseGitUrl; |
| Dave Borowitz | ded109a | 2014-03-03 15:25:39 -0500 | [diff] [blame] | 98 | protected final Config baseConfig; |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 99 | protected final FileResolver<HttpServletRequest> resolver; |
| 100 | protected final HttpServletRequest req; |
| 101 | |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 102 | protected DefaultAccess( |
| 103 | File basePath, |
| 104 | String canonicalBasePath, |
| 105 | String baseGitUrl, |
| 106 | Config baseConfig, |
| 107 | FileResolver<HttpServletRequest> resolver, |
| 108 | HttpServletRequest req) { |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 109 | this.basePath = checkNotNull(basePath, "basePath"); |
| 110 | this.canonicalBasePath = checkNotNull(canonicalBasePath, "canonicalBasePath"); |
| 111 | this.baseGitUrl = checkNotNull(baseGitUrl, "baseGitUrl"); |
| Dave Borowitz | ded109a | 2014-03-03 15:25:39 -0500 | [diff] [blame] | 112 | this.baseConfig = checkNotNull(baseConfig, "baseConfig"); |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 113 | this.resolver = checkNotNull(resolver, "resolver"); |
| 114 | this.req = checkNotNull(req, "req"); |
| 115 | } |
| 116 | |
| 117 | @Override |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 118 | public Map<String, RepositoryDescription> listRepositories(String prefix, Set<String> branches) |
| 119 | throws IOException { |
| Dave Borowitz | 9e95e63 | 2013-11-27 16:16:24 -0500 | [diff] [blame] | 120 | Map<String, RepositoryDescription> repos = Maps.newTreeMap(US_COLLATOR); |
| Shawn Pearce | c709c4c | 2015-08-28 15:30:42 -0700 | [diff] [blame] | 121 | for (Repository repo : scanRepositories(basePath, prefix, req)) { |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 122 | repos.put(getRepositoryName(repo), buildDescription(repo, branches)); |
| 123 | repo.close(); |
| 124 | } |
| 125 | return repos; |
| 126 | } |
| 127 | |
| 128 | @Override |
| 129 | public Object getUserKey() { |
| 130 | // Always return the same anonymous user key (effectively running with the |
| 131 | // same user permissions as the JVM). Subclasses may override this behavior. |
| 132 | return ANONYMOUS_USER_KEY; |
| 133 | } |
| 134 | |
| 135 | @Override |
| 136 | public String getRepositoryName() { |
| 137 | return getRepositoryName(ServletUtils.getRepository(req)); |
| 138 | } |
| 139 | |
| 140 | @Override |
| 141 | public RepositoryDescription getRepositoryDescription() throws IOException { |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 142 | return buildDescription(ServletUtils.getRepository(req), Collections.<String>emptySet()); |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 143 | } |
| 144 | |
| Dave Borowitz | ded109a | 2014-03-03 15:25:39 -0500 | [diff] [blame] | 145 | @Override |
| 146 | public Config getConfig() { |
| 147 | return baseConfig; |
| 148 | } |
| 149 | |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 150 | private String getRepositoryName(Repository repo) { |
| 151 | String path = getRelativePath(repo); |
| 152 | if (repo.isBare() && path.endsWith(".git")) { |
| 153 | path = path.substring(0, path.length() - 4); |
| 154 | } |
| 155 | return path; |
| 156 | } |
| 157 | |
| 158 | private String getRelativePath(Repository repo) { |
| 159 | String path = repo.isBare() ? repo.getDirectory().getPath() : repo.getDirectory().getParent(); |
| 160 | if (repo.isBare()) { |
| 161 | path = repo.getDirectory().getPath(); |
| 162 | if (path.endsWith(".git")) { |
| 163 | path = path.substring(0, path.length() - 4); |
| 164 | } |
| 165 | } else { |
| 166 | path = repo.getDirectory().getParent(); |
| 167 | } |
| 168 | return getRelativePath(path); |
| 169 | } |
| 170 | |
| 171 | private String getRelativePath(String path) { |
| 172 | String base = basePath.getPath(); |
| Matt Giuca | 696812f | 2016-02-18 11:13:42 +1100 | [diff] [blame] | 173 | if (path.equals(base)) { |
| 174 | return ""; |
| 175 | } |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 176 | if (path.startsWith(base)) { |
| 177 | return path.substring(base.length() + 1); |
| 178 | } |
| 179 | if (path.startsWith(canonicalBasePath)) { |
| 180 | return path.substring(canonicalBasePath.length() + 1); |
| 181 | } |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 182 | throw new IllegalStateException( |
| 183 | String.format("Repository path %s is outside base path %s", path, base)); |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 184 | } |
| 185 | |
| 186 | private String loadDescriptionText(Repository repo) throws IOException { |
| 187 | String desc = null; |
| 188 | StoredConfig config = repo.getConfig(); |
| 189 | IOException configError = null; |
| 190 | try { |
| 191 | config.load(); |
| 192 | desc = config.getString("gitweb", null, "description"); |
| 193 | } catch (ConfigInvalidException e) { |
| 194 | configError = new IOException(e); |
| 195 | } |
| 196 | if (desc == null) { |
| 197 | File descFile = new File(repo.getDirectory(), "description"); |
| 198 | if (descFile.exists()) { |
| 199 | desc = new String(IO.readFully(descFile)); |
| Shawn Pearce | 5f3e73b | 2016-03-08 13:54:57 -0800 | [diff] [blame] | 200 | if (DEFAULT_DESCRIPTION.equals(CharMatcher.whitespace().trimFrom(desc))) { |
| Dave Borowitz | fa0fef0 | 2013-01-07 13:17:53 -0800 | [diff] [blame] | 201 | desc = null; |
| 202 | } |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 203 | } else if (configError != null) { |
| 204 | throw configError; |
| 205 | } |
| 206 | } |
| 207 | return desc; |
| 208 | } |
| 209 | |
| 210 | private RepositoryDescription buildDescription(Repository repo, Set<String> branches) |
| 211 | throws IOException { |
| 212 | RepositoryDescription desc = new RepositoryDescription(); |
| 213 | desc.name = getRepositoryName(repo); |
| 214 | desc.cloneUrl = baseGitUrl + getRelativePath(repo); |
| 215 | desc.description = loadDescriptionText(repo); |
| 216 | if (!branches.isEmpty()) { |
| 217 | desc.branches = Maps.newLinkedHashMap(); |
| 218 | for (String name : branches) { |
| Dave Borowitz | 14cad73 | 2016-05-26 17:34:19 -0400 | [diff] [blame] | 219 | Ref ref = repo.exactRef(normalizeRefName(name)); |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 220 | if ((ref != null) && (ref.getObjectId() != null)) { |
| 221 | desc.branches.put(name, ref.getObjectId().name()); |
| 222 | } |
| 223 | } |
| 224 | } |
| 225 | return desc; |
| 226 | } |
| 227 | |
| 228 | private static String normalizeRefName(String name) { |
| 229 | if (name.startsWith("refs/")) { |
| 230 | return name; |
| 231 | } |
| 232 | return "refs/heads/" + name; |
| 233 | } |
| 234 | |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 235 | private Collection<Repository> scanRepositories( |
| 236 | File basePath, String prefix, HttpServletRequest req) throws IOException { |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 237 | List<Repository> repos = Lists.newArrayList(); |
| Shawn Pearce | c709c4c | 2015-08-28 15:30:42 -0700 | [diff] [blame] | 238 | Queue<File> todo = initScan(basePath, prefix); |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 239 | while (!todo.isEmpty()) { |
| 240 | File file = todo.remove(); |
| 241 | try { |
| 242 | repos.add(resolver.open(req, getRelativePath(file.getPath()))); |
| 243 | } catch (RepositoryNotFoundException e) { |
| 244 | File[] children = file.listFiles(); |
| 245 | if (children != null) { |
| Dave Borowitz | 2705893 | 2014-12-03 15:44:46 -0800 | [diff] [blame] | 246 | Collections.addAll(todo, children); |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 247 | } |
| 248 | } catch (ServiceNotEnabledException e) { |
| 249 | throw new IOException(e); |
| 250 | } |
| 251 | } |
| 252 | return repos; |
| 253 | } |
| Shawn Pearce | c709c4c | 2015-08-28 15:30:42 -0700 | [diff] [blame] | 254 | |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 255 | private Queue<File> initScan(File basePath, String prefix) throws IOException { |
| Shawn Pearce | c709c4c | 2015-08-28 15:30:42 -0700 | [diff] [blame] | 256 | Queue<File> todo = Queues.newArrayDeque(); |
| 257 | File[] entries; |
| 258 | if (isValidPrefix(prefix)) { |
| 259 | entries = new File(basePath, CharMatcher.is('/').trimFrom(prefix)).listFiles(); |
| 260 | } else { |
| 261 | entries = basePath.listFiles(); |
| 262 | } |
| 263 | if (entries != null) { |
| 264 | Collections.addAll(todo, entries); |
| 265 | } else if (!basePath.isDirectory()) { |
| 266 | throw new IOException("base path is not a directory: " + basePath.getPath()); |
| 267 | } |
| 268 | return todo; |
| 269 | } |
| 270 | |
| 271 | private static boolean isValidPrefix(String prefix) { |
| 272 | return !Strings.isNullOrEmpty(prefix) |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 273 | && !prefix.equals(".") |
| 274 | && !prefix.equals("..") |
| Shawn Pearce | c709c4c | 2015-08-28 15:30:42 -0700 | [diff] [blame] | 275 | && !prefix.contains("../") |
| 276 | && !prefix.endsWith("/.."); |
| 277 | } |
| Dave Borowitz | 9de6595 | 2012-08-13 16:09:45 -0700 | [diff] [blame] | 278 | } |