Line data Source code
1 : // Copyright (C) 2014 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.sshd.commands;
16 :
17 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
18 : import static java.nio.charset.StandardCharsets.UTF_8;
19 :
20 : import com.google.common.base.Splitter;
21 : import com.google.common.collect.ImmutableMap;
22 : import com.google.gerrit.extensions.restapi.AuthException;
23 : import com.google.gerrit.server.change.ArchiveFormatInternal;
24 : import com.google.gerrit.server.permissions.PermissionBackend;
25 : import com.google.gerrit.server.permissions.PermissionBackendException;
26 : import com.google.gerrit.server.permissions.ProjectPermission;
27 : import com.google.gerrit.server.project.ProjectCache;
28 : import com.google.gerrit.server.project.ProjectState;
29 : import com.google.gerrit.server.restapi.change.AllowedFormats;
30 : import com.google.gerrit.server.restapi.project.CommitsCollection;
31 : import com.google.gerrit.sshd.AbstractGitCommand;
32 : import com.google.inject.Inject;
33 : import java.io.IOException;
34 : import java.util.ArrayList;
35 : import java.util.Arrays;
36 : import java.util.Collections;
37 : import java.util.List;
38 : import java.util.Map;
39 : import org.eclipse.jgit.api.ArchiveCommand;
40 : import org.eclipse.jgit.api.errors.GitAPIException;
41 : import org.eclipse.jgit.lib.ObjectId;
42 : import org.eclipse.jgit.revwalk.RevCommit;
43 : import org.eclipse.jgit.revwalk.RevWalk;
44 : import org.eclipse.jgit.transport.PacketLineIn;
45 : import org.eclipse.jgit.transport.PacketLineOut;
46 : import org.eclipse.jgit.transport.SideBandOutputStream;
47 : import org.kohsuke.args4j.Argument;
48 : import org.kohsuke.args4j.CmdLineException;
49 : import org.kohsuke.args4j.CmdLineParser;
50 : import org.kohsuke.args4j.Option;
51 : import org.kohsuke.args4j.ParserProperties;
52 :
53 : /** Allows getting archives for Git repositories over SSH using the Git upload-archive protocol. */
54 1 : public class UploadArchive extends AbstractGitCommand {
55 : /**
56 : * Options for parsing Git commands.
57 : *
58 : * <p>These options are not passed on command line, but received through input stream in pkt-line
59 : * format.
60 : */
61 1 : static class Options {
62 1 : @Option(
63 : name = "-f",
64 : aliases = {"--format"},
65 : usage =
66 : "Format of the"
67 : + " resulting archive: tar or zip... If this option is not given, and"
68 : + " the output file is specified, the format is inferred from the"
69 : + " filename if possible (e.g. writing to \"foo.zip\" makes the output"
70 : + " to be in the zip format). Otherwise the output format is tar.")
71 : private String format = "tar";
72 :
73 : @Option(name = "--prefix", usage = "Prepend <prefix>/ to each filename in the archive.")
74 : private String prefix;
75 :
76 1 : @Option(
77 : name = "--compression-level",
78 : usage =
79 : "Controls compression for different formats. The value is in [0-9] with 0 for fast levels"
80 : + " with medium compressions, and 9 for the highest compression. Note that higher"
81 : + " compressions require more memory.")
82 : private int compressionLevel = -1;
83 :
84 : @Option(name = "-0", usage = "Store the files instead of deflating them.")
85 : private boolean level0;
86 :
87 : @Option(name = "-1")
88 : private boolean level1;
89 :
90 : @Option(name = "-2")
91 : private boolean level2;
92 :
93 : @Option(name = "-3")
94 : private boolean level3;
95 :
96 : @Option(name = "-4")
97 : private boolean level4;
98 :
99 : @Option(name = "-5")
100 : private boolean level5;
101 :
102 : @Option(name = "-6")
103 : private boolean level6;
104 :
105 : @Option(name = "-7")
106 : private boolean level7;
107 :
108 : @Option(name = "-8")
109 : private boolean level8;
110 :
111 : @Option(
112 : name = "-9",
113 : usage =
114 : "Highest and slowest compression level. You "
115 : + "can specify any number from 1 to 9 to adjust compression speed and "
116 : + "ratio.")
117 : private boolean level9;
118 :
119 1 : @Argument(index = 0, required = true, usage = "The tree or commit to produce an archive for.")
120 : private String treeIsh = "master";
121 :
122 : @Argument(
123 : index = 1,
124 : multiValued = true,
125 : usage =
126 : "Without an optional path parameter, all files and subdirectories of "
127 : + "the current working directory are included in the archive. If one "
128 : + "or more paths are specified, only these are included.")
129 : private List<String> path;
130 : }
131 :
132 : @Inject private PermissionBackend permissionBackend;
133 : @Inject private CommitsCollection commits;
134 : @Inject private AllowedFormats allowedFormats;
135 : @Inject private ProjectCache projectCache;
136 1 : private Options options = new Options();
137 :
138 : /**
139 : * Read and parse arguments from input stream. This method gets the arguments from input stream,
140 : * in Pkt-line format, then parses them to fill the options object.
141 : */
142 : protected void readArguments() throws IOException, Failure {
143 1 : String argCmd = "argument ";
144 1 : List<String> args = new ArrayList<>();
145 :
146 : // Read arguments in Pkt-Line format
147 1 : PacketLineIn packetIn = new PacketLineIn(in);
148 : for (; ; ) {
149 1 : String s = packetIn.readString();
150 1 : if (PacketLineIn.isEnd(s)) {
151 1 : break;
152 : }
153 1 : if (!s.startsWith(argCmd)) {
154 0 : throw new Failure(1, "fatal: 'argument' token or flush expected, got " + s);
155 : }
156 1 : for (String p : Splitter.on('=').limit(2).split(s.substring(argCmd.length()))) {
157 1 : args.add(p);
158 1 : }
159 1 : }
160 :
161 : try {
162 : // Parse them into the 'options' field
163 1 : CmdLineParser parser =
164 1 : new CmdLineParser(options, ParserProperties.defaults().withAtSyntax(false));
165 1 : parser.parseArgument(args);
166 1 : if (options.path == null || Arrays.asList(".").equals(options.path)) {
167 0 : options.path = Collections.emptyList();
168 : }
169 0 : } catch (CmdLineException e) {
170 0 : throw new Failure(2, "fatal: unable to parse arguments, " + e);
171 1 : }
172 1 : }
173 :
174 : @Override
175 : protected void runImpl() throws IOException, PermissionBackendException, Failure {
176 1 : PacketLineOut packetOut = new PacketLineOut(out);
177 1 : packetOut.setFlushOnEnd(true);
178 1 : packetOut.writeString("ACK");
179 1 : packetOut.end();
180 :
181 : try {
182 : // Parse Git arguments
183 1 : readArguments();
184 :
185 1 : ArchiveFormatInternal f = allowedFormats.getExtensions().get("." + options.format);
186 1 : if (f == null) {
187 1 : throw new Failure(3, "fatal: upload-archive not permitted for format " + options.format);
188 : }
189 :
190 : // Find out the object to get from the specified reference and paths
191 1 : ObjectId treeId = repo.resolve(options.treeIsh);
192 1 : if (treeId == null) {
193 0 : throw new Failure(4, "fatal: reference not found: " + options.treeIsh);
194 : }
195 :
196 : // Verify the user has permissions to read the specified tree.
197 1 : if (!canRead(treeId)) {
198 0 : throw new Failure(5, "fatal: no permission to read tree" + options.treeIsh);
199 : }
200 :
201 : // The archive is sent in DATA sideband channel
202 1 : try (SideBandOutputStream sidebandOut =
203 : new SideBandOutputStream(
204 : SideBandOutputStream.CH_DATA, SideBandOutputStream.MAX_BUF, out)) {
205 1 : new ArchiveCommand(repo)
206 1 : .setFormat(f.name())
207 1 : .setFormatOptions(getFormatOptions(f))
208 1 : .setTree(treeId)
209 1 : .setPaths(options.path.toArray(new String[0]))
210 1 : .setPrefix(options.prefix)
211 1 : .setOutputStream(sidebandOut)
212 1 : .call();
213 1 : sidebandOut.flush();
214 0 : } catch (GitAPIException e) {
215 0 : throw new Failure(7, "fatal: git api exception, " + e);
216 1 : }
217 1 : } catch (Exception e) {
218 : // Report the error in ERROR sideband channel. Catch Throwable too so we can also catch
219 : // NoClassDefFound.
220 1 : try (SideBandOutputStream sidebandError =
221 : new SideBandOutputStream(
222 : SideBandOutputStream.CH_ERROR, SideBandOutputStream.MAX_BUF, out)) {
223 1 : sidebandError.write(e.getMessage().getBytes(UTF_8));
224 1 : sidebandError.flush();
225 : }
226 1 : throw e;
227 : } finally {
228 : // In any case, cleanly close the packetOut channel
229 1 : packetOut.end();
230 : }
231 1 : }
232 :
233 : private Map<String, Object> getFormatOptions(ArchiveFormatInternal f) {
234 1 : if (options.compressionLevel != -1) {
235 1 : return ImmutableMap.of("compression-level", options.compressionLevel);
236 : }
237 0 : if (f == ArchiveFormatInternal.ZIP) {
238 0 : int value =
239 0 : Arrays.asList(
240 0 : options.level0,
241 0 : options.level1,
242 0 : options.level2,
243 0 : options.level3,
244 0 : options.level4,
245 0 : options.level5,
246 0 : options.level6,
247 0 : options.level7,
248 0 : options.level8,
249 0 : options.level9)
250 0 : .indexOf(true);
251 0 : if (value >= 0) {
252 0 : return ImmutableMap.of("level", Integer.valueOf(value));
253 : }
254 : }
255 0 : return Collections.emptyMap();
256 : }
257 :
258 : private boolean canRead(ObjectId revId) throws IOException, PermissionBackendException {
259 1 : ProjectState projectState =
260 1 : projectCache.get(projectName).orElseThrow(illegalState(projectName));
261 :
262 1 : if (!projectState.statePermitsRead()) {
263 0 : return false;
264 : }
265 :
266 : try {
267 1 : permissionBackend.user(user).project(projectName).check(ProjectPermission.READ);
268 1 : return true;
269 0 : } catch (AuthException e) {
270 : // Check reachability of the specific revision.
271 0 : try (RevWalk rw = new RevWalk(repo)) {
272 0 : RevCommit commit = rw.parseCommit(revId);
273 0 : return commits.canRead(projectState, repo, commit);
274 : }
275 : }
276 : }
277 : }
|