Line data Source code
1 : // Copyright (C) 2009 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.httpd.raw; 16 : 17 : import static com.google.gerrit.server.project.ProjectCache.illegalState; 18 : 19 : import com.google.gerrit.entities.Change; 20 : import com.google.gerrit.entities.Patch; 21 : import com.google.gerrit.entities.PatchSet; 22 : import com.google.gerrit.extensions.restapi.AuthException; 23 : import com.google.gerrit.extensions.restapi.ResourceConflictException; 24 : import com.google.gerrit.extensions.restapi.Url; 25 : import com.google.gerrit.server.PatchSetUtil; 26 : import com.google.gerrit.server.edit.ChangeEdit; 27 : import com.google.gerrit.server.edit.ChangeEditUtil; 28 : import com.google.gerrit.server.notedb.ChangeNotes; 29 : import com.google.gerrit.server.permissions.ChangePermission; 30 : import com.google.gerrit.server.permissions.PermissionBackend; 31 : import com.google.gerrit.server.permissions.PermissionBackendException; 32 : import com.google.gerrit.server.project.NoSuchChangeException; 33 : import com.google.gerrit.server.project.ProjectCache; 34 : import com.google.inject.Inject; 35 : import com.google.inject.Singleton; 36 : import java.io.IOException; 37 : import java.util.Optional; 38 : import javax.servlet.http.HttpServlet; 39 : import javax.servlet.http.HttpServletRequest; 40 : import javax.servlet.http.HttpServletResponse; 41 : import org.eclipse.jgit.lib.ObjectId; 42 : 43 : /** 44 : * Exports a single version of a patch as a normal file download. 45 : * 46 : * <p>This can be relatively unsafe with Microsoft Internet Explorer 6.0 as the browser will (rather 47 : * incorrectly) treat an HTML or JavaScript file its supposed to download as though it was served by 48 : * this site, and will execute it with the site's own protection domain. This opens a massive 49 : * security hole so we package the content into a zip file. 50 : */ 51 : @Singleton 52 : public class CatServlet extends HttpServlet { 53 : private static final long serialVersionUID = 1L; 54 : 55 : private final ChangeEditUtil changeEditUtil; 56 : private final PatchSetUtil psUtil; 57 : private final ChangeNotes.Factory changeNotesFactory; 58 : private final PermissionBackend permissionBackend; 59 : private final ProjectCache projectCache; 60 : 61 : @Inject 62 : CatServlet( 63 : ChangeEditUtil ceu, 64 : PatchSetUtil psu, 65 : ChangeNotes.Factory cnf, 66 : PermissionBackend pb, 67 99 : ProjectCache pc) { 68 99 : changeEditUtil = ceu; 69 99 : psUtil = psu; 70 99 : changeNotesFactory = cnf; 71 99 : permissionBackend = pb; 72 99 : projectCache = pc; 73 99 : } 74 : 75 : @Override 76 : protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { 77 0 : String keyStr = req.getPathInfo(); 78 : 79 : // We shouldn't have to do this extra decode pass, but somehow we 80 : // are now receiving our "^1" suffix as "%5E1", which confuses us 81 : // downstream. Other times we get our embedded "," as "%2C", which 82 : // is equally bad. And yet when these happen a "%2F" is left as-is, 83 : // rather than escaped as "%252F", which makes me feel really really 84 : // uncomfortable with a blind decode right here. 85 : // 86 0 : keyStr = Url.decode(keyStr); 87 : 88 0 : if (!keyStr.startsWith("/")) { 89 0 : rsp.sendError(HttpServletResponse.SC_NOT_FOUND); 90 0 : return; 91 : } 92 0 : keyStr = keyStr.substring(1); 93 : 94 : final Patch.Key patchKey; 95 : final int side; 96 : { 97 0 : final int c = keyStr.lastIndexOf('^'); 98 0 : if (c == 0) { 99 0 : rsp.sendError(HttpServletResponse.SC_NOT_FOUND); 100 0 : return; 101 : } 102 : 103 0 : if (c < 0) { 104 0 : side = 0; 105 : } else { 106 : try { 107 0 : side = Integer.parseInt(keyStr.substring(c + 1)); 108 0 : keyStr = keyStr.substring(0, c); 109 0 : } catch (NumberFormatException e) { 110 0 : rsp.sendError(HttpServletResponse.SC_NOT_FOUND); 111 0 : return; 112 0 : } 113 : } 114 : 115 : try { 116 0 : patchKey = Patch.Key.parse(keyStr); 117 0 : } catch (NumberFormatException e) { 118 0 : rsp.sendError(HttpServletResponse.SC_NOT_FOUND); 119 0 : return; 120 0 : } 121 : } 122 : 123 0 : final Change.Id changeId = patchKey.patchSetId().changeId(); 124 : String revision; 125 : try { 126 0 : ChangeNotes notes = changeNotesFactory.createCheckedUsingIndexLookup(changeId); 127 0 : permissionBackend.currentUser().change(notes).check(ChangePermission.READ); 128 0 : projectCache 129 0 : .get(notes.getProjectName()) 130 0 : .orElseThrow(illegalState(notes.getProjectName())) 131 0 : .checkStatePermitsRead(); 132 0 : if (patchKey.patchSetId().get() == 0) { 133 : // change edit 134 0 : Optional<ChangeEdit> edit = changeEditUtil.byChange(notes); 135 0 : if (edit.isPresent()) { 136 0 : revision = ObjectId.toString(edit.get().getEditCommit()); 137 : } else { 138 0 : rsp.sendError(HttpServletResponse.SC_NOT_FOUND); 139 0 : return; 140 : } 141 0 : } else { 142 0 : PatchSet patchSet = psUtil.get(notes, patchKey.patchSetId()); 143 0 : if (patchSet == null) { 144 0 : rsp.sendError(HttpServletResponse.SC_NOT_FOUND); 145 0 : return; 146 : } 147 0 : revision = patchSet.commitId().name(); 148 : } 149 0 : } catch (ResourceConflictException | NoSuchChangeException | AuthException e) { 150 0 : rsp.sendError(HttpServletResponse.SC_NOT_FOUND); 151 0 : return; 152 0 : } catch (PermissionBackendException | IOException e) { 153 0 : getServletContext().log("Cannot query database", e); 154 0 : rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 155 0 : return; 156 0 : } 157 : 158 0 : String path = patchKey.fileName(); 159 0 : String restUrl = 160 0 : String.format( 161 : "%s/changes/%d/revisions/%s/files/%s/download?parent=%d", 162 0 : req.getContextPath(), changeId.get(), revision, Url.encode(path), side); 163 0 : rsp.sendRedirect(restUrl); 164 0 : } 165 : }