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