Line data Source code
1 : // Copyright (C) 2013 The Android Open Source Project
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.gerrit.server.change;
16 :
17 : import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
18 :
19 : import com.google.common.base.Strings;
20 : import com.google.common.hash.Hasher;
21 : import com.google.common.hash.Hashing;
22 : import com.google.gerrit.common.Nullable;
23 : import com.google.gerrit.common.data.PatchScript.FileMode;
24 : import com.google.gerrit.entities.Patch;
25 : import com.google.gerrit.extensions.restapi.BadRequestException;
26 : import com.google.gerrit.extensions.restapi.BinaryResult;
27 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
28 : import com.google.gerrit.server.git.GitRepositoryManager;
29 : import com.google.gerrit.server.mime.FileTypeRegistry;
30 : import com.google.gerrit.server.project.ProjectState;
31 : import com.google.gerrit.server.util.time.TimeUtil;
32 : import com.google.inject.Inject;
33 : import com.google.inject.Singleton;
34 : import eu.medsea.mimeutil.MimeType;
35 : import java.io.IOException;
36 : import java.io.OutputStream;
37 : import java.security.SecureRandom;
38 : import java.util.zip.ZipEntry;
39 : import java.util.zip.ZipOutputStream;
40 : import org.eclipse.jgit.errors.LargeObjectException;
41 : import org.eclipse.jgit.errors.RepositoryNotFoundException;
42 : import org.eclipse.jgit.lib.Constants;
43 : import org.eclipse.jgit.lib.ObjectId;
44 : import org.eclipse.jgit.lib.ObjectLoader;
45 : import org.eclipse.jgit.lib.Repository;
46 : import org.eclipse.jgit.revwalk.RevCommit;
47 : import org.eclipse.jgit.revwalk.RevWalk;
48 : import org.eclipse.jgit.treewalk.TreeWalk;
49 : import org.eclipse.jgit.util.NB;
50 :
51 : @Singleton
52 : public class FileContentUtil {
53 : public static final String TEXT_X_GERRIT_COMMIT_MESSAGE = "text/x-gerrit-commit-message";
54 : public static final String TEXT_X_GERRIT_MERGE_LIST = "text/x-gerrit-merge-list";
55 : private static final String X_GIT_SYMLINK = "x-git/symlink";
56 : private static final String X_GIT_GITLINK = "x-git/gitlink";
57 : private static final int MAX_SIZE = 5 << 20;
58 : private static final String ZIP_TYPE = "application/zip";
59 145 : private static final SecureRandom rng = new SecureRandom();
60 :
61 : private final GitRepositoryManager repoManager;
62 : private final FileTypeRegistry registry;
63 :
64 : @Inject
65 145 : FileContentUtil(GitRepositoryManager repoManager, FileTypeRegistry ftr) {
66 145 : this.repoManager = repoManager;
67 145 : this.registry = ftr;
68 145 : }
69 :
70 : /**
71 : * Get the content of a file at a specific commit or one of it's parent commits.
72 : *
73 : * @param project A {@code Project} that this request refers to.
74 : * @param revstr An {@code ObjectId} specifying the commit.
75 : * @param path A string specifying the filepath.
76 : * @param parent A 1-based parent index to get the content from instead. Null if the content
77 : * should be obtained from {@code revstr} instead.
78 : * @return Content of the file as {@code BinaryResult}.
79 : */
80 : public BinaryResult getContent(
81 : ProjectState project, ObjectId revstr, String path, @Nullable Integer parent)
82 : throws BadRequestException, ResourceNotFoundException, IOException {
83 17 : try (Repository repo = openRepository(project);
84 17 : RevWalk rw = new RevWalk(repo)) {
85 17 : if (parent != null) {
86 1 : RevCommit revCommit = rw.parseCommit(revstr);
87 1 : if (revCommit == null) {
88 0 : throw new ResourceNotFoundException("commit not found");
89 : }
90 1 : if (parent > revCommit.getParentCount()) {
91 1 : throw new BadRequestException("invalid parent");
92 : }
93 1 : revstr = rw.parseCommit(revstr).getParent(Integer.max(0, parent - 1)).toObjectId();
94 : }
95 17 : return getContent(repo, project, revstr, path);
96 : }
97 : }
98 :
99 : public BinaryResult getContent(
100 : Repository repo, ProjectState project, ObjectId revstr, String path)
101 : throws IOException, ResourceNotFoundException, BadRequestException {
102 17 : try (RevWalk rw = new RevWalk(repo)) {
103 17 : RevCommit commit = rw.parseCommit(revstr);
104 17 : try (TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), path, commit.getTree())) {
105 17 : if (tw == null) {
106 4 : throw new ResourceNotFoundException();
107 : }
108 :
109 17 : org.eclipse.jgit.lib.FileMode mode = tw.getFileMode(0);
110 17 : ObjectId id = tw.getObjectId(0);
111 17 : if (mode == org.eclipse.jgit.lib.FileMode.GITLINK) {
112 0 : return BinaryResult.create(id.name()).setContentType(X_GIT_GITLINK).base64();
113 : }
114 :
115 17 : if (mode == org.eclipse.jgit.lib.FileMode.TREE) {
116 1 : throw new BadRequestException("cannot retrieve content of directories");
117 : }
118 :
119 17 : ObjectLoader obj = repo.open(id, OBJ_BLOB);
120 : byte[] raw;
121 : try {
122 17 : raw = obj.getCachedBytes(MAX_SIZE);
123 0 : } catch (LargeObjectException e) {
124 0 : raw = null;
125 17 : }
126 :
127 : String type;
128 17 : if (mode == org.eclipse.jgit.lib.FileMode.SYMLINK) {
129 0 : type = X_GIT_SYMLINK;
130 : } else {
131 17 : type = registry.getMimeType(path, raw).toString();
132 17 : type = resolveContentType(project, path, FileMode.FILE, type);
133 : }
134 :
135 17 : return asBinaryResult(raw, obj).setContentType(type).base64();
136 0 : }
137 0 : }
138 : }
139 :
140 : private static BinaryResult asBinaryResult(byte[] raw, ObjectLoader obj) {
141 17 : if (raw != null) {
142 17 : return BinaryResult.create(raw);
143 : }
144 0 : BinaryResult result =
145 0 : new BinaryResult() {
146 : @Override
147 : public void writeTo(OutputStream os) throws IOException {
148 0 : obj.copyTo(os);
149 0 : }
150 : };
151 0 : result.setContentLength(obj.getSize());
152 0 : return result;
153 : }
154 :
155 : public BinaryResult downloadContent(
156 : ProjectState project, ObjectId revstr, String path, @Nullable Integer parent)
157 : throws ResourceNotFoundException, IOException {
158 1 : try (Repository repo = openRepository(project);
159 1 : RevWalk rw = new RevWalk(repo)) {
160 1 : String suffix = "new";
161 1 : RevCommit commit = rw.parseCommit(revstr);
162 1 : if (parent != null && parent > 0) {
163 0 : if (commit.getParentCount() == 1) {
164 0 : suffix = "old";
165 : } else {
166 0 : suffix = "old" + parent;
167 : }
168 0 : commit = rw.parseCommit(commit.getParent(parent - 1));
169 : }
170 1 : try (TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), path, commit.getTree())) {
171 1 : if (tw == null) {
172 0 : throw new ResourceNotFoundException();
173 : }
174 :
175 1 : int mode = tw.getFileMode(0).getObjectType();
176 1 : if (mode != Constants.OBJ_BLOB) {
177 0 : throw new ResourceNotFoundException();
178 : }
179 :
180 1 : ObjectId id = tw.getObjectId(0);
181 1 : ObjectLoader obj = repo.open(id, OBJ_BLOB);
182 : byte[] raw;
183 : try {
184 1 : raw = obj.getCachedBytes(MAX_SIZE);
185 0 : } catch (LargeObjectException e) {
186 0 : raw = null;
187 1 : }
188 :
189 1 : MimeType contentType = registry.getMimeType(path, raw);
190 1 : return registry.isSafeInline(contentType)
191 0 : ? wrapBlob(path, obj, raw, contentType, suffix)
192 1 : : zipBlob(path, obj, commit, suffix);
193 : }
194 : }
195 : }
196 :
197 : private BinaryResult wrapBlob(
198 : String path,
199 : final ObjectLoader obj,
200 : byte[] raw,
201 : MimeType contentType,
202 : @Nullable String suffix) {
203 0 : return asBinaryResult(raw, obj)
204 0 : .setContentType(contentType.toString())
205 0 : .setAttachmentName(safeFileName(path, suffix));
206 : }
207 :
208 : @SuppressWarnings("resource")
209 : private BinaryResult zipBlob(
210 : final String path, ObjectLoader obj, RevCommit commit, @Nullable final String suffix) {
211 1 : final String commitName = commit.getName();
212 1 : final long when = commit.getCommitTime() * 1000L;
213 1 : return new BinaryResult() {
214 : @Override
215 : public void writeTo(OutputStream os) throws IOException {
216 1 : try (ZipOutputStream zipOut = new ZipOutputStream(os)) {
217 1 : String decoration = randSuffix();
218 1 : if (!Strings.isNullOrEmpty(suffix)) {
219 1 : decoration = suffix + '-' + decoration;
220 : }
221 1 : ZipEntry e = new ZipEntry(safeFileName(path, decoration));
222 1 : e.setComment(commitName + ":" + path);
223 1 : e.setSize(obj.getSize());
224 1 : e.setTime(when);
225 1 : zipOut.putNextEntry(e);
226 1 : obj.copyTo(zipOut);
227 1 : zipOut.closeEntry();
228 : }
229 1 : }
230 1 : }.setContentType(ZIP_TYPE).setAttachmentName(safeFileName(path, suffix) + ".zip").disableGzip();
231 : }
232 :
233 : private static String safeFileName(String fileName, @Nullable String suffix) {
234 : // Convert a file path (e.g. "src/Init.c") to a safe file name with
235 : // no meta-characters that might be unsafe on any given platform.
236 : //
237 1 : int slash = fileName.lastIndexOf('/');
238 1 : if (slash >= 0) {
239 0 : fileName = fileName.substring(slash + 1);
240 : }
241 :
242 1 : StringBuilder r = new StringBuilder(fileName.length());
243 1 : for (int i = 0; i < fileName.length(); i++) {
244 1 : final char c = fileName.charAt(i);
245 1 : if (c == '_' || c == '-' || c == '.' || c == '@') {
246 1 : r.append(c);
247 1 : } else if ('0' <= c && c <= '9') {
248 0 : r.append(c);
249 1 : } else if ('A' <= c && c <= 'Z') {
250 0 : r.append(c);
251 1 : } else if ('a' <= c && c <= 'z') {
252 1 : r.append(c);
253 0 : } else if (c == ' ' || c == '\n' || c == '\r' || c == '\t') {
254 0 : r.append('-');
255 : } else {
256 0 : r.append('_');
257 : }
258 : }
259 1 : fileName = r.toString();
260 :
261 1 : int ext = fileName.lastIndexOf('.');
262 1 : if (suffix == null) {
263 0 : return fileName;
264 1 : } else if (ext <= 0) {
265 0 : return fileName + "_" + suffix;
266 : } else {
267 1 : return fileName.substring(0, ext) + "_" + suffix + fileName.substring(ext);
268 : }
269 : }
270 :
271 : private static String randSuffix() {
272 : // Produce a random suffix that is difficult (or nearly impossible)
273 : // for an attacker to guess in advance. This reduces the risk that
274 : // an attacker could upload a *.class file and have us send a ZIP
275 : // that can be invoked through an applet tag in the victim's browser.
276 : //
277 1 : Hasher h = Hashing.murmur3_128().newHasher();
278 1 : byte[] buf = new byte[8];
279 :
280 1 : NB.encodeInt64(buf, 0, TimeUtil.nowMs());
281 1 : h.putBytes(buf);
282 :
283 1 : rng.nextBytes(buf);
284 1 : h.putBytes(buf);
285 :
286 1 : return h.hash().toString();
287 : }
288 :
289 : public static String resolveContentType(
290 : ProjectState project, String path, FileMode fileMode, String mimeType) {
291 21 : switch (fileMode) {
292 : case FILE:
293 21 : if (Patch.COMMIT_MSG.equals(path)) {
294 4 : return TEXT_X_GERRIT_COMMIT_MESSAGE;
295 : }
296 21 : if (Patch.MERGE_LIST.equals(path)) {
297 3 : return TEXT_X_GERRIT_MERGE_LIST;
298 : }
299 20 : if (project != null) {
300 20 : for (ProjectState p : project.tree()) {
301 20 : String t = p.getConfig().getMimeTypes().getMimeType(path);
302 20 : if (t != null) {
303 0 : return t;
304 : }
305 20 : }
306 : }
307 20 : return mimeType;
308 : case GITLINK:
309 1 : return X_GIT_GITLINK;
310 : case SYMLINK:
311 0 : return X_GIT_SYMLINK;
312 : default:
313 0 : throw new IllegalStateException("file mode: " + fileMode);
314 : }
315 : }
316 :
317 : private Repository openRepository(ProjectState project)
318 : throws RepositoryNotFoundException, IOException {
319 17 : return repoManager.openRepository(project.getNameKey());
320 : }
321 : }
|