Line data Source code
1 : /* 2 : * Licensed to the Apache Software Foundation (ASF) under one or more 3 : * contributor license agreements. See the NOTICE file distributed with this 4 : * work for additional information regarding copyright ownership. The ASF 5 : * licenses this file to you under the Apache License, Version 2.0 (the 6 : * "License"); you may not use this file except in compliance with the License. 7 : * You may obtain a copy of the License at 8 : * 9 : * http://www.apache.org/licenses/LICENSE-2.0 10 : * 11 : * Unless required by applicable law or agreed to in writing, software 12 : * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 : * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 : * License for the specific language governing permissions and limitations under 15 : * the License. 16 : */ 17 : 18 : /* 19 : * NB: This code was primarly ripped out of MINA SSHD. 20 : * 21 : * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> 22 : */ 23 : package com.google.gerrit.sshd.commands; 24 : 25 : import static java.nio.charset.StandardCharsets.UTF_8; 26 : 27 : import com.google.common.flogger.FluentLogger; 28 : import com.google.gerrit.server.AccessPath; 29 : import com.google.gerrit.server.tools.ToolsCatalog; 30 : import com.google.gerrit.sshd.BaseCommand; 31 : import com.google.inject.Inject; 32 : import java.io.ByteArrayOutputStream; 33 : import java.io.FileNotFoundException; 34 : import java.io.IOException; 35 : import java.io.UnsupportedEncodingException; 36 : import org.apache.sshd.server.Environment; 37 : import org.apache.sshd.server.channel.ChannelSession; 38 : 39 0 : final class ScpCommand extends BaseCommand { 40 0 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 41 : 42 : private static final String TYPE_DIR = "D"; 43 : private static final String TYPE_FILE = "C"; 44 : 45 : private boolean opt_r; 46 : private boolean opt_t; 47 : private boolean opt_f; 48 : private String root; 49 : 50 : @Inject private ToolsCatalog toc; 51 : private IOException error; 52 : 53 : @Override 54 : public void setArguments(String[] args) { 55 0 : root = ""; 56 0 : for (int i = 0; i < args.length; i++) { 57 0 : if (args[i].charAt(0) == '-') { 58 0 : for (int j = 1; j < args[i].length(); j++) { 59 0 : switch (args[i].charAt(j)) { 60 : case 'f': 61 0 : opt_f = true; 62 0 : break; 63 : case 'p': 64 0 : break; 65 : case 'r': 66 0 : opt_r = true; 67 0 : break; 68 : case 't': 69 0 : opt_t = true; 70 0 : break; 71 : case 'v': 72 : break; 73 : } 74 : } 75 0 : } else if (i == args.length - 1) { 76 0 : root = args[args.length - 1]; 77 : } 78 : } 79 0 : if (!opt_f && !opt_t) { 80 0 : error = new IOException("Either -f or -t option should be set"); 81 : } 82 0 : } 83 : 84 : @Override 85 : public void start(ChannelSession channel, Environment env) { 86 0 : startThread(this::runImp, AccessPath.SSH_COMMAND); 87 0 : } 88 : 89 : private void runImp() { 90 : try { 91 0 : readAck(); 92 0 : if (error != null) { 93 0 : throw error; 94 : } 95 : 96 0 : if (opt_f) { 97 0 : if (root.startsWith("/")) { 98 0 : root = root.substring(1); 99 : } 100 0 : if (root.endsWith("/")) { 101 0 : root = root.substring(0, root.length() - 1); 102 : } 103 0 : if (root.equals(".")) { 104 0 : root = ""; 105 : } 106 : 107 0 : final ToolsCatalog.Entry ent = toc.get(root); 108 0 : if (ent == null) { 109 0 : throw new IOException(root + " not found"); 110 : 111 0 : } else if (ToolsCatalog.Entry.Type.FILE == ent.getType()) { 112 0 : readFile(ent); 113 : 114 0 : } else if (ToolsCatalog.Entry.Type.DIR == ent.getType()) { 115 0 : if (!opt_r) { 116 0 : throw new IOException(root + " not a regular file"); 117 : } 118 0 : readDir(ent); 119 : } else { 120 0 : throw new IOException(root + " not supported"); 121 : } 122 0 : } else { 123 0 : throw new IOException("Unsupported mode"); 124 : } 125 0 : } catch (IOException e) { 126 0 : if (e.getClass() == IOException.class && "Pipe closed".equals(e.getMessage())) { 127 : // Ignore a pipe closed error, its the user disconnecting from us 128 : // while we are waiting for them to stalk. 129 : // 130 0 : return; 131 : } 132 : 133 : try { 134 0 : out.write(2); 135 0 : out.write(e.getMessage().getBytes(UTF_8)); 136 0 : out.write('\n'); 137 0 : out.flush(); 138 0 : } catch (IOException e2) { 139 : // Ignore 140 0 : } 141 0 : logger.atFine().withCause(e).log("Error in scp command"); 142 0 : } 143 0 : } 144 : 145 : private String readLine() throws IOException { 146 0 : ByteArrayOutputStream baos = new ByteArrayOutputStream(); 147 : for (; ; ) { 148 0 : int c = in.read(); 149 0 : if (c == '\n') { 150 0 : return baos.toString(UTF_8); 151 0 : } else if (c == -1) { 152 0 : throw new IOException("End of stream"); 153 : } else { 154 0 : baos.write(c); 155 : } 156 0 : } 157 : } 158 : 159 : private void readFile(ToolsCatalog.Entry ent) throws IOException { 160 0 : byte[] data = ent.getBytes(); 161 0 : if (data == null) { 162 0 : throw new FileNotFoundException(ent.getPath()); 163 : } 164 : 165 0 : header(ent, data.length); 166 0 : readAck(); 167 : 168 0 : out.write(data); 169 0 : ack(); 170 0 : readAck(); 171 0 : } 172 : 173 : private void readDir(ToolsCatalog.Entry dir) throws IOException { 174 0 : header(dir, 0); 175 0 : readAck(); 176 : 177 0 : for (ToolsCatalog.Entry e : dir.getChildren()) { 178 0 : if (ToolsCatalog.Entry.Type.DIR == e.getType()) { 179 0 : readDir(e); 180 : } else { 181 0 : readFile(e); 182 : } 183 0 : } 184 : 185 0 : out.write("E\n".getBytes(UTF_8)); 186 0 : out.flush(); 187 0 : readAck(); 188 0 : } 189 : 190 : private void header(ToolsCatalog.Entry dir, int len) 191 : throws IOException, UnsupportedEncodingException { 192 0 : final StringBuilder buf = new StringBuilder(); 193 0 : switch (dir.getType()) { 194 : case DIR: 195 0 : buf.append(TYPE_DIR); 196 0 : break; 197 : case FILE: 198 0 : buf.append(TYPE_FILE); 199 : break; 200 : } 201 0 : buf.append("0").append(Integer.toOctalString(dir.getMode())); // perms 202 0 : buf.append(" "); 203 0 : buf.append(len); // length 204 0 : buf.append(" "); 205 0 : buf.append(dir.getName()); 206 0 : buf.append("\n"); 207 0 : out.write(buf.toString().getBytes(UTF_8)); 208 0 : out.flush(); 209 0 : } 210 : 211 : private void ack() throws IOException { 212 0 : out.write(0); 213 0 : out.flush(); 214 0 : } 215 : 216 : private void readAck() throws IOException { 217 0 : switch (in.read()) { 218 : case 0: 219 0 : break; 220 : case 1: 221 0 : logger.atFine().log("Received warning: %s", readLine()); 222 0 : break; 223 : case 2: 224 0 : throw new IOException("Received nack: " + readLine()); 225 : } 226 0 : } 227 : }