| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one or more | |
| * contributor license agreements. See the NOTICE file distributed with this | |
| * work for additional information regarding copyright ownership. The ASF | |
| * licenses this file to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
| * License for the specific language governing permissions and limitations under | |
| * the License. | |
| */ | |
| /* | |
| * NB: This code was primarly ripped out of MINA SSHD. | |
| * | |
| * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> | |
| */ | |
| package com.google.gerrit.sshd.commands; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import com.google.common.flogger.FluentLogger; | |
| import com.google.gerrit.server.AccessPath; | |
| import com.google.gerrit.server.tools.ToolsCatalog; | |
| import com.google.gerrit.sshd.BaseCommand; | |
| import com.google.inject.Inject; | |
| import java.io.ByteArrayOutputStream; | |
| import java.io.FileNotFoundException; | |
| import java.io.IOException; | |
| import java.io.UnsupportedEncodingException; | |
| import org.apache.sshd.server.Environment; | |
| import org.apache.sshd.server.channel.ChannelSession; | |
| final class ScpCommand extends BaseCommand { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| private static final String TYPE_DIR = "D"; | |
| private static final String TYPE_FILE = "C"; | |
| private boolean opt_r; | |
| private boolean opt_t; | |
| private boolean opt_f; | |
| private String root; | |
| @Inject private ToolsCatalog toc; | |
| private IOException error; | |
| @Override | |
| public void setArguments(String[] args) { | |
| root = ""; | |
| for (int i = 0; i < args.length; i++) { | |
| if (args[i].charAt(0) == '-') { | |
| for (int j = 1; j < args[i].length(); j++) { | |
| switch (args[i].charAt(j)) { | |
| case 'f': | |
| opt_f = true; | |
| break; | |
| case 'p': | |
| break; | |
| case 'r': | |
| opt_r = true; | |
| break; | |
| case 't': | |
| opt_t = true; | |
| break; | |
| case 'v': | |
| break; | |
| } | |
| } | |
| } else if (i == args.length - 1) { | |
| root = args[args.length - 1]; | |
| } | |
| } | |
| if (!opt_f && !opt_t) { | |
| error = new IOException("Either -f or -t option should be set"); | |
| } | |
| } | |
| @Override | |
| public void start(ChannelSession channel, Environment env) { | |
| startThread(this::runImp, AccessPath.SSH_COMMAND); | |
| } | |
| private void runImp() { | |
| try { | |
| readAck(); | |
| if (error != null) { | |
| throw error; | |
| } | |
| if (opt_f) { | |
| if (root.startsWith("/")) { | |
| root = root.substring(1); | |
| } | |
| if (root.endsWith("/")) { | |
| root = root.substring(0, root.length() - 1); | |
| } | |
| if (root.equals(".")) { | |
| root = ""; | |
| } | |
| final ToolsCatalog.Entry ent = toc.get(root); | |
| if (ent == null) { | |
| throw new IOException(root + " not found"); | |
| } else if (ToolsCatalog.Entry.Type.FILE == ent.getType()) { | |
| readFile(ent); | |
| } else if (ToolsCatalog.Entry.Type.DIR == ent.getType()) { | |
| if (!opt_r) { | |
| throw new IOException(root + " not a regular file"); | |
| } | |
| readDir(ent); | |
| } else { | |
| throw new IOException(root + " not supported"); | |
| } | |
| } else { | |
| throw new IOException("Unsupported mode"); | |
| } | |
| } catch (IOException e) { | |
| if (e.getClass() == IOException.class && "Pipe closed".equals(e.getMessage())) { | |
| // Ignore a pipe closed error, its the user disconnecting from us | |
| // while we are waiting for them to stalk. | |
| // | |
| return; | |
| } | |
| try { | |
| out.write(2); | |
| out.write(e.getMessage().getBytes(UTF_8)); | |
| out.write('\n'); | |
| out.flush(); | |
| } catch (IOException e2) { | |
| // Ignore | |
| } | |
| logger.atFine().withCause(e).log("Error in scp command"); | |
| } | |
| } | |
| private String readLine() throws IOException { | |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |
| for (; ; ) { | |
| int c = in.read(); | |
| if (c == '\n') { | |
| return baos.toString(UTF_8); | |
| } else if (c == -1) { | |
| throw new IOException("End of stream"); | |
| } else { | |
| baos.write(c); | |
| } | |
| } | |
| } | |
| private void readFile(ToolsCatalog.Entry ent) throws IOException { | |
| byte[] data = ent.getBytes(); | |
| if (data == null) { | |
| throw new FileNotFoundException(ent.getPath()); | |
| } | |
| header(ent, data.length); | |
| readAck(); | |
| out.write(data); | |
| ack(); | |
| readAck(); | |
| } | |
| private void readDir(ToolsCatalog.Entry dir) throws IOException { | |
| header(dir, 0); | |
| readAck(); | |
| for (ToolsCatalog.Entry e : dir.getChildren()) { | |
| if (ToolsCatalog.Entry.Type.DIR == e.getType()) { | |
| readDir(e); | |
| } else { | |
| readFile(e); | |
| } | |
| } | |
| out.write("E\n".getBytes(UTF_8)); | |
| out.flush(); | |
| readAck(); | |
| } | |
| private void header(ToolsCatalog.Entry dir, int len) | |
| throws IOException, UnsupportedEncodingException { | |
| final StringBuilder buf = new StringBuilder(); | |
| switch (dir.getType()) { | |
| case DIR: | |
| buf.append(TYPE_DIR); | |
| break; | |
| case FILE: | |
| buf.append(TYPE_FILE); | |
| break; | |
| } | |
| buf.append("0").append(Integer.toOctalString(dir.getMode())); // perms | |
| buf.append(" "); | |
| buf.append(len); // length | |
| buf.append(" "); | |
| buf.append(dir.getName()); | |
| buf.append("\n"); | |
| out.write(buf.toString().getBytes(UTF_8)); | |
| out.flush(); | |
| } | |
| private void ack() throws IOException { | |
| out.write(0); | |
| out.flush(); | |
| } | |
| private void readAck() throws IOException { | |
| switch (in.read()) { | |
| case 0: | |
| break; | |
| case 1: | |
| logger.atFine().log("Received warning: %s", readLine()); | |
| break; | |
| case 2: | |
| throw new IOException("Received nack: " + readLine()); | |
| } | |
| } | |
| } |