/**
* 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.camel.component.scp;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Hashtable;
import java.util.List;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UIKeyboardInteractive;
import com.jcraft.jsch.UserInfo;
import org.apache.camel.Exchange;
import org.apache.camel.InvalidPayloadException;
import org.apache.camel.component.file.GenericFileEndpoint;
import org.apache.camel.component.file.GenericFileOperationFailedException;
import org.apache.camel.component.file.remote.RemoteFileConfiguration;
import org.apache.camel.component.file.remote.RemoteFileOperations;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* SCP remote file operations
*/
public class ScpOperations implements RemoteFileOperations<ScpFile> {
private static final Logger LOG = LoggerFactory.getLogger(ScpOperations.class);
private ScpEndpoint endpoint;
private Session session;
private ChannelExec channel;
private String userKnownHostFile;
@Override
public void setEndpoint(GenericFileEndpoint<ScpFile> endpoint) {
this.endpoint = (ScpEndpoint)endpoint;
}
@Override
public boolean deleteFile(String name) throws GenericFileOperationFailedException {
throw new GenericFileOperationFailedException("Operation 'delete' not supported by the scp: protocol");
}
@Override
public boolean existsFile(String name) throws GenericFileOperationFailedException {
// maybe... cannot determine using the scp: protocol
return false;
}
@Override
public boolean renameFile(String from, String to) throws GenericFileOperationFailedException {
throw new GenericFileOperationFailedException("Operation 'rename' not supported by the scp: protocol");
}
@Override
public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException {
// done by the server
return true;
}
@Override
public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
return false;
}
@Override
public void releaseRetreivedFileResources(Exchange exchange) throws GenericFileOperationFailedException {
// No-op
}
@Override
public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
ObjectHelper.notNull(session, "session");
ScpConfiguration cfg = endpoint.getConfiguration();
int timeout = cfg.getConnectTimeout();
if (LOG.isTraceEnabled()) {
LOG.trace("Opening channel to {} with {} timeout...", cfg.remoteServerInformation(),
timeout > 0 ? (Integer.toString(timeout) + " ms") : "no");
}
String file = getRemoteFile(name, cfg);
InputStream is = null;
if (exchange.getIn().getBody() == null) {
// Do an explicit test for a null body and decide what to do
if (endpoint.isAllowNullBody()) {
LOG.trace("Writing empty file.");
is = new ByteArrayInputStream(new byte[]{});
} else {
throw new GenericFileOperationFailedException("Cannot write null body to file: " + name);
}
}
try {
channel = (ChannelExec) session.openChannel("exec");
channel.setCommand(getScpCommand(cfg, file));
channel.connect(timeout);
LOG.trace("Channel connected to {}", cfg.remoteServerInformation());
try {
if (is == null) {
is = exchange.getIn().getMandatoryBody(InputStream.class);
}
write(channel, file, is, cfg);
} catch (InvalidPayloadException e) {
throw new GenericFileOperationFailedException("Cannot store file: " + name, e);
} catch (IOException e) {
throw new GenericFileOperationFailedException("Failed to write file " + file, e);
} finally {
// must close stream after usage
IOHelper.close(is);
}
} catch (JSchException e) {
throw new GenericFileOperationFailedException("Failed to write file " + file, e);
} finally {
if (channel != null) {
LOG.trace("Disconnecting 'exec' scp channel");
channel.disconnect();
channel = null;
LOG.trace("Channel disconnected from {}", cfg.remoteServerInformation());
}
}
return true;
}
@Override
public String getCurrentDirectory() throws GenericFileOperationFailedException {
return endpoint.getConfiguration().getDirectory();
}
@Override
public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException {
throw new GenericFileOperationFailedException("Operation 'cd " + path + "' not supported by the scp: protocol");
}
@Override
public void changeToParentDirectory() throws GenericFileOperationFailedException {
throw new GenericFileOperationFailedException("Operation 'cd ..' not supported by the scp: protocol");
}
@Override
public List<ScpFile> listFiles() throws GenericFileOperationFailedException {
throw new GenericFileOperationFailedException("Operation 'ls' not supported by the scp: protocol");
}
@Override
public List<ScpFile> listFiles(String path) throws GenericFileOperationFailedException {
throw new GenericFileOperationFailedException("Operation 'ls " + path + "' not supported by the scp: protocol");
}
@Override
public boolean connect(RemoteFileConfiguration configuration) throws GenericFileOperationFailedException {
if (!isConnected()) {
session = createSession(configuration instanceof ScpConfiguration ? (ScpConfiguration)configuration : null);
// TODO: deal with reconnection attempts
if (!isConnected()) {
session = null;
throw new GenericFileOperationFailedException("Failed to connect to " + configuration.remoteServerInformation());
}
}
return true;
}
@Override
public boolean isConnected() throws GenericFileOperationFailedException {
return session != null && session.isConnected();
}
@Override
public void disconnect() throws GenericFileOperationFailedException {
if (isConnected()) {
session.disconnect();
}
session = null;
}
@Override
public boolean sendNoop() throws GenericFileOperationFailedException {
// not supported by scp:
return true;
}
@Override
public boolean sendSiteCommand(String command) throws GenericFileOperationFailedException {
return true;
}
private Session createSession(ScpConfiguration config) {
ObjectHelper.notNull(config, "ScpConfiguration");
try {
final JSch jsch = new JSch();
// get from configuration
if (ObjectHelper.isNotEmpty(config.getCiphers())) {
LOG.trace("Using ciphers: {}", config.getCiphers());
Hashtable<String, String> ciphers = new Hashtable<String, String>();
ciphers.put("cipher.s2c", config.getCiphers());
ciphers.put("cipher.c2s", config.getCiphers());
JSch.setConfig(ciphers);
}
if (ObjectHelper.isNotEmpty(config.getPrivateKeyFile())) {
LOG.trace("Using private keyfile: {}", config.getPrivateKeyFile());
String pkfp = config.getPrivateKeyFilePassphrase();
jsch.addIdentity(config.getPrivateKeyFile(), ObjectHelper.isNotEmpty(pkfp) ? pkfp : null);
}
String knownHostsFile = config.getKnownHostsFile();
if (knownHostsFile == null && config.isUseUserKnownHostsFile()) {
if (userKnownHostFile == null) {
userKnownHostFile = System.getProperty("user.home") + "/.ssh/known_hosts";
LOG.info("Known host file not configured, using user known host file: " + userKnownHostFile);
}
knownHostsFile = userKnownHostFile;
}
jsch.setKnownHosts(ObjectHelper.isEmpty(knownHostsFile) ? null : knownHostsFile);
session = jsch.getSession(config.getUsername(), config.getHost(), config.getPort());
session.setTimeout(config.getTimeout());
session.setUserInfo(new SessionUserInfo(config));
if (ObjectHelper.isNotEmpty(config.getStrictHostKeyChecking())) {
LOG.trace("Using StrickHostKeyChecking: {}", config.getStrictHostKeyChecking());
session.setConfig("StrictHostKeyChecking", config.getStrictHostKeyChecking());
}
if (ObjectHelper.isNotEmpty(config.getPreferredAuthentications())) {
LOG.trace("Using preferredAuthentications: {}", config.getPreferredAuthentications());
session.setConfig("PreferredAuthentications", config.getPreferredAuthentications());
}
int timeout = config.getConnectTimeout();
LOG.debug("Connecting to {} with {} timeout...", config.remoteServerInformation(),
timeout > 0 ? (Integer.toString(timeout) + " ms") : "no");
if (timeout > 0) {
session.connect(timeout);
} else {
session.connect();
}
} catch (JSchException e) {
session = null;
LOG.warn("Could not create ssh session for " + config.remoteServerInformation(), e);
}
return session;
}
private void write(ChannelExec c, String name, InputStream data, ScpConfiguration cfg) throws IOException {
OutputStream os = c.getOutputStream();
InputStream is = c.getInputStream();
try {
writeFile(name, data, os, is, cfg);
} finally {
IOHelper.close(is, os);
}
}
private void writeFile(String filename, InputStream data, OutputStream os, InputStream is, ScpConfiguration cfg) throws IOException {
final int lineFeed = '\n';
String bytes;
int pos = filename.indexOf('/');
if (pos >= 0) {
// write to child directory
String dir = filename.substring(0, pos);
bytes = "D0775 0 " + dir;
LOG.trace("[scp:sink] {}", bytes);
os.write(bytes.getBytes());
os.write(lineFeed);
os.flush();
readAck(is);
writeFile(filename.substring(pos + 1), data, os, is, cfg);
bytes = "E";
LOG.trace("[scp:sink] {}", bytes);
os.write(bytes.getBytes());
os.write(lineFeed);
os.flush();
readAck(is);
} else {
int count = 0;
int read;
int size = endpoint.getBufferSize();
byte[] reply = new byte[size];
// figure out the stream size as we need to pass it in the header
BufferedInputStream buffer = new BufferedInputStream(data, size);
try {
buffer.mark(Integer.MAX_VALUE);
while ((read = buffer.read(reply)) != -1) {
count += read;
}
// send the header
bytes = "C0" + cfg.getChmod() + " " + count + " " + filename;
LOG.trace("[scp:sink] {}", bytes);
os.write(bytes.getBytes());
os.write(lineFeed);
os.flush();
readAck(is);
// now send the stream
buffer.reset();
while ((read = buffer.read(reply)) != -1) {
os.write(reply, 0, read);
}
writeAck(os);
readAck(is);
} finally {
IOHelper.close(buffer);
}
}
}
private void writeAck(OutputStream os) throws IOException {
os.write(0);
os.flush();
}
private int readAck(InputStream is) throws IOException {
String message;
int answer = is.read();
switch (answer) {
case 0:
break;
default:
message = "[scp] Return Code [" + answer + "] " + readLine(is);
throw new IOException(message);
}
return answer;
}
private String readLine(InputStream is) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
int c;
do {
c = is.read();
if (c == '\n') {
return bytes.toString();
}
bytes.write(c);
} while (c != -1);
} finally {
IOHelper.close(bytes);
}
String message = "[scp] Unexpected end of stream";
throw new IOException(message);
}
private static String getRemoteTarget(ScpConfiguration config) {
// use current dir (".") if target directory not specified in uri
return config.getDirectory().isEmpty() ? "." : config.getDirectory();
}
private static String getRemoteFile(String name, ScpConfiguration config) {
String dir = config.getDirectory();
dir = dir.endsWith("/") ? dir : dir + "/";
return name.startsWith(dir) ? name.substring(dir.length()) : name;
}
private static boolean isRecursiveScp(String name) {
return name.indexOf('/') > 0;
}
private static String getScpCommand(ScpConfiguration config, String name) {
StringBuilder cmd = new StringBuilder();
cmd.append("scp ");
// TODO: need config for scp *-p* (preserves modification times, access times, and modes from the original file)
// String command="scp " + (ptimestamp ? "-p " : "") + "-t " + configuration.getDirectory();
// TODO: refactor to use generic command
cmd.append(isRecursiveScp(name) ? "-r " : "");
cmd.append("-t ");
cmd.append(getRemoteTarget(config));
return cmd.toString();
}
protected static final class SessionUserInfo implements UserInfo, UIKeyboardInteractive {
private final ScpConfiguration config;
public SessionUserInfo(ScpConfiguration config) {
ObjectHelper.notNull(config, "config");
this.config = config;
}
@Override
public String getPassphrase() {
LOG.warn("Private Key authentication not supported");
return null;
}
@Override
public String getPassword() {
LOG.debug("Providing password for ssh authentication of user '{}'", config.getUsername());
return config.getPassword();
}
@Override
public boolean promptPassword(String message) {
LOG.debug(message);
return true;
}
@Override
public boolean promptPassphrase(String message) {
LOG.debug(message);
return true;
}
@Override
public boolean promptYesNo(String message) {
LOG.debug(message);
return false;
}
@Override
public void showMessage(String message) {
LOG.debug(message);
}
@Override
public String[] promptKeyboardInteractive(String destination, String name,
String instruction, String[] prompt, boolean[] echo) {
LOG.debug(instruction);
// Called for either SSH_MSG_USERAUTH_INFO_REQUEST or SSH_MSG_USERAUTH_PASSWD_CHANGEREQ
// The most secure choice (especially for the second case) is to return null
return null;
}
}
}