blob: 978f225e7ff00d661051d910fcd65334ac471393 [file] [log] [blame]
/*
* 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.
*/
package org.apache.maven.wagon.providers.ssh.jsch;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import org.apache.maven.wagon.CommandExecutionException;
import org.apache.maven.wagon.InputData;
import org.apache.maven.wagon.OutputData;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.events.TransferEvent;
import org.apache.maven.wagon.providers.ssh.ScpHelper;
import org.apache.maven.wagon.repository.RepositoryPermissions;
import org.apache.maven.wagon.resource.Resource;
/**
* SCP protocol wagon.
* <p/>
* Note that this implementation is <i>not</i> thread-safe, and multiple channels can not be used on the session at
* the same time.
* <p/>
* See <a href="http://blogs.sun.com/janp/entry/how_the_scp_protocol_works">
* http://blogs.sun.com/janp/entry/how_the_scp_protocol_works</a>
* for information on how the SCP protocol works.
*
*
* @todo [BP] add compression flag
* @plexus.component role="org.apache.maven.wagon.Wagon"
* role-hint="scp"
* instantiation-strategy="per-lookup"
*/
public class ScpWagon extends AbstractJschWagon {
private static final char COPY_START_CHAR = 'C';
private static final char ACK_SEPARATOR = ' ';
private static final String END_OF_FILES_MSG = "E\n";
private static final int LINE_BUFFER_SIZE = 8192;
private static final byte LF = '\n';
private ChannelExec channel;
private InputStream channelInputStream;
private OutputStream channelOutputStream;
private void setFileGroup(RepositoryPermissions permissions, String basedir, Resource resource)
throws CommandExecutionException {
if (permissions != null && permissions.getGroup() != null) {
// executeCommand( "chgrp -f " + permissions.getGroup() + " " + getPath( basedir, resource.getName() ) );
executeCommand("chgrp -f " + permissions.getGroup() + " \"" + getPath(basedir, resource.getName()) + "\"");
}
}
protected void cleanupPutTransfer(Resource resource) {
if (channel != null) {
channel.disconnect();
channel = null;
}
}
protected void finishPutTransfer(Resource resource, InputStream input, OutputStream output)
throws TransferFailedException {
try {
sendEom(output);
checkAck(channelInputStream);
// This came from SCPClient in Ganymede SSH2. It is sent after all files.
output.write(END_OF_FILES_MSG.getBytes());
output.flush();
} catch (IOException e) {
handleIOException(resource, e);
}
String basedir = getRepository().getBasedir();
try {
setFileGroup(getRepository().getPermissions(), basedir, resource);
} catch (CommandExecutionException e) {
fireTransferError(resource, e, TransferEvent.REQUEST_PUT);
throw new TransferFailedException(e.getMessage(), e);
}
}
private void checkAck(InputStream in) throws IOException {
int code = in.read();
if (code == -1) {
throw new IOException("Unexpected end of data");
} else if (code == 1) {
String line = readLine(in);
throw new IOException("SCP terminated with error: '" + line + "'");
} else if (code == 2) {
throw new IOException("SCP terminated with error (code: " + code + ")");
} else if (code != 0) {
throw new IOException("SCP terminated with unknown error code");
}
}
protected void finishGetTransfer(Resource resource, InputStream input, OutputStream output)
throws TransferFailedException {
try {
checkAck(input);
sendEom(channelOutputStream);
} catch (IOException e) {
handleGetException(resource, e);
}
}
protected void cleanupGetTransfer(Resource resource) {
if (channel != null) {
channel.disconnect();
}
}
@Deprecated
protected void getTransfer(
Resource resource, OutputStream output, InputStream input, boolean closeInput, int maxSize)
throws TransferFailedException {
super.getTransfer(resource, output, input, closeInput, (int) resource.getContentLength());
}
protected void getTransfer(
Resource resource, OutputStream output, InputStream input, boolean closeInput, long maxSize)
throws TransferFailedException {
super.getTransfer(resource, output, input, closeInput, resource.getContentLength());
}
protected String readLine(InputStream in) throws IOException {
StringBuilder sb = new StringBuilder();
while (true) {
if (sb.length() > LINE_BUFFER_SIZE) {
throw new IOException("Remote server sent a too long line");
}
int c = in.read();
if (c < 0) {
throw new IOException("Remote connection terminated unexpectedly.");
}
if (c == LF) {
break;
}
sb.append((char) c);
}
return sb.toString();
}
protected static void sendEom(OutputStream out) throws IOException {
out.write(0);
out.flush();
}
public void fillInputData(InputData inputData) throws TransferFailedException, ResourceDoesNotExistException {
Resource resource = inputData.getResource();
String path = getPath(getRepository().getBasedir(), resource.getName());
// String cmd = "scp -p -f " + path;
String cmd = "scp -p -f \"" + path + "\"";
fireTransferDebug("Executing command: " + cmd);
try {
channel = (ChannelExec) session.openChannel(EXEC_CHANNEL);
channel.setCommand(cmd);
// get I/O streams for remote scp
channelOutputStream = channel.getOutputStream();
InputStream in = channel.getInputStream();
inputData.setInputStream(in);
channel.connect();
sendEom(channelOutputStream);
int exitCode = in.read();
if (exitCode == 'T') {
String line = readLine(in);
String[] times = line.split(" ");
resource.setLastModified(Long.valueOf(times[0]).longValue() * 1000);
sendEom(channelOutputStream);
exitCode = in.read();
}
String line = readLine(in);
if (exitCode != COPY_START_CHAR) {
if (exitCode == 1
&& (line.contains("No such file or directory")
|| line.indexOf("no such file or directory") != 1)) {
throw new ResourceDoesNotExistException(line);
} else {
throw new IOException("Exit code: " + exitCode + " - " + line);
}
}
if (line == null) {
throw new EOFException("Unexpected end of data");
}
String perms = line.substring(0, 4);
fireTransferDebug("Remote file permissions: " + perms);
if (line.charAt(4) != ACK_SEPARATOR && line.charAt(5) != ACK_SEPARATOR) {
throw new IOException("Invalid transfer header: " + line);
}
int index = line.indexOf(ACK_SEPARATOR, 5);
if (index < 0) {
throw new IOException("Invalid transfer header: " + line);
}
long filesize = Long.parseLong(line.substring(5, index));
fireTransferDebug("Remote file size: " + filesize);
resource.setContentLength(filesize);
String filename = line.substring(index + 1);
fireTransferDebug("Remote filename: " + filename);
sendEom(channelOutputStream);
} catch (JSchException e) {
handleGetException(resource, e);
} catch (IOException e) {
handleGetException(resource, e);
}
}
public void fillOutputData(OutputData outputData) throws TransferFailedException {
Resource resource = outputData.getResource();
String basedir = getRepository().getBasedir();
String path = getPath(basedir, resource.getName());
String dir = ScpHelper.getResourceDirectory(resource.getName());
try {
sshTool.createRemoteDirectories(
getPath(basedir, dir), getRepository().getPermissions());
} catch (CommandExecutionException e) {
fireTransferError(resource, e, TransferEvent.REQUEST_PUT);
throw new TransferFailedException(e.getMessage(), e);
}
String octalMode = getOctalMode(getRepository().getPermissions());
// exec 'scp -p -t rfile' remotely
String command = "scp";
if (octalMode != null) {
command += " -p";
}
command += " -t \"" + path + "\"";
fireTransferDebug("Executing command: " + command);
String resourceName = resource.getName();
OutputStream out = null;
try {
channel = (ChannelExec) session.openChannel(EXEC_CHANNEL);
channel.setCommand(command);
// get I/O streams for remote scp
out = channel.getOutputStream();
outputData.setOutputStream(out);
channelInputStream = channel.getInputStream();
channel.connect();
checkAck(channelInputStream);
// send "C0644 filesize filename", where filename should not include '/'
long filesize = resource.getContentLength();
String mode = octalMode == null ? "0644" : octalMode;
command = "C" + mode + " " + filesize + " ";
if (resourceName.lastIndexOf(ScpHelper.PATH_SEPARATOR) > 0) {
command += resourceName.substring(resourceName.lastIndexOf(ScpHelper.PATH_SEPARATOR) + 1);
} else {
command += resourceName;
}
command += "\n";
out.write(command.getBytes());
out.flush();
checkAck(channelInputStream);
} catch (JSchException e) {
fireTransferError(resource, e, TransferEvent.REQUEST_PUT);
String msg = "Error occurred while deploying '" + resourceName + "' to remote repository: "
+ getRepository().getUrl() + ": " + e.getMessage();
throw new TransferFailedException(msg, e);
} catch (IOException e) {
handleIOException(resource, e);
}
}
private void handleIOException(Resource resource, IOException e) throws TransferFailedException {
if (e.getMessage().contains("set mode: Operation not permitted")) {
fireTransferDebug(e.getMessage());
} else {
fireTransferError(resource, e, TransferEvent.REQUEST_PUT);
String msg = "Error occurred while deploying '" + resource.getName() + "' to remote repository: "
+ getRepository().getUrl() + ": " + e.getMessage();
throw new TransferFailedException(msg, e);
}
}
public String getOctalMode(RepositoryPermissions permissions) {
String mode = null;
if (permissions != null && permissions.getFileMode() != null) {
if (permissions.getFileMode().matches("[0-9]{3,4}")) {
mode = permissions.getFileMode();
if (mode.length() == 3) {
mode = "0" + mode;
}
} else {
// TODO: calculate?
// TODO: as warning
fireSessionDebug("Not using non-octal permissions: " + permissions.getFileMode());
}
}
return mode;
}
}