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