/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.embedded.ssh.internal;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.FileSystem;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.sshd.common.file.FileSystemAware;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
import org.apache.sshd.server.SessionAware;
import org.apache.sshd.server.scp.ScpCommand;
import org.apache.sshd.server.scp.ScpCommandFactory;
import org.apache.sshd.server.session.ServerSession;
import de.rcenvironment.core.authentication.AuthenticationException;
import de.rcenvironment.core.embedded.ssh.api.ScpContext;
import de.rcenvironment.core.embedded.ssh.api.ScpContextManager;
import de.rcenvironment.core.utils.common.StringUtils;
/**
* Class for pre-processing SCP commands before handing them over to the native {@link ScpCommand}.
*
* This is necessary because Apache Mina does not delegate an ScpCommand for pre-processing. This is not done in the SshFactory because this
* the currently active user is necessary to decide if the destination is valid or not.
*
* @author Sebastian Holtappels
* @author Robert Mischke
* @author Brigitte Boden
*/
public class ScpCommandWrapper implements Command, FileSystemAware, SessionAware {
private static final int NOT_FOUND_INDEX = -1; // indexOf() result on no match
private static final String FORWARD_SLASH = "/";
private static final String BACKSLASH = "\\";
private String command;
private InputStream in;
private OutputStream err;
private OutputStream out;
private ExitCallback callback;
private FileSystem fileSystem;
private ScpCommandFactory scpCommandFactory;
private ScpContextManager scpContextManager;
private ServerSession session;
private final Log logger = LogFactory.getLog(getClass());
public ScpCommandWrapper(String command, ScpContextManager authenticationManager) {
this.command = command;
this.scpContextManager = authenticationManager;
this.scpCommandFactory = new ScpCommandFactory();
}
@Override
public void start(Environment env) throws IOException {
String username = env.getEnv().get(Environment.ENV_USER);
try {
String virtualScpPath = getScpPathOfCommand();
// if (isValidScpPath(userName)) {
ScpContext scpContext = scpContextManager.getMatchingScpContext(username, virtualScpPath);
if (scpContext != null) {
try {
delegateToScp(env, scpContext);
} catch (IOException e) {
logger.warn("Exception in SCP command wrapper", e);
throw e;
}
} else {
throw new AuthenticationException(StringUtils.format("No permission to access SCP path \"%s\"", virtualScpPath));
}
} catch (AuthenticationException e) {
logger.warn("Denied SCP access for user " + username + ": " + e.toString());
// close the connection like ScpCommand#run() does on an IOException
// TODO sometimes, the pscp client shows "connection lost" instead of this error message; reason unclear - misc_ro
out.write(2);
out.write(("ERROR: " + e.getMessage()).getBytes());
out.write('\n');
out.flush();
callback.onExit(0);
}
}
@Override
public void destroy() {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
logger.debug(e);
}
}
private void delegateToScp(Environment env, ScpContext scpContext) throws IOException {
ScpCommand scpCommand = (ScpCommand) scpCommandFactory.createCommand(rewriteCommand(command, scpContext));
scpCommand.setErrorStream(err);
scpCommand.setExitCallback(callback);
scpCommand.setInputStream(in);
scpCommand.setOutputStream(out);
scpCommand.setSession(session);
scpCommand.setFileSystem(fileSystem);
scpCommand.start(env);
}
private String rewriteCommand(String originalCommand, ScpContext scpContext) {
// TODO make stricter by regexp parsing - misc_ro
int startOfPath = originalCommand.indexOf(FORWARD_SLASH);
String originalCommandStart = originalCommand.substring(0, startOfPath);
String originalPath = originalCommand.substring(startOfPath);
logger.debug(StringUtils.format("Rewriting access to logical file path '%s' (command prefix: '%s')",
originalPath, originalCommandStart));
if (!originalPath.startsWith(scpContext.getVirtualScpRootPath())) {
throw new IllegalStateException("Virtual SCP path '" + originalPath + "' does not start with expected root path '"
+ scpContext.getVirtualScpRootPath() + "'");
}
String relativePath = originalPath.substring(scpContext.getVirtualScpRootPath().length());
if (relativePath.startsWith(FORWARD_SLASH) || relativePath.startsWith(BACKSLASH)) {
relativePath = relativePath.substring(1);
}
String rewrittenPath = new File(scpContext.getLocalRootPath(), relativePath).getAbsolutePath().replace(BACKSLASH, FORWARD_SLASH);
// needed to make MINA accept the rewritten windows path
// TODO cross-check on linux
if (!rewrittenPath.startsWith(FORWARD_SLASH)) {
rewrittenPath = FORWARD_SLASH + rewrittenPath;
}
// maintain trailing slash, if it exists, as they get lost in local file operations
if (originalPath.endsWith(FORWARD_SLASH) && !rewrittenPath.endsWith(FORWARD_SLASH)) {
rewrittenPath = rewrittenPath + FORWARD_SLASH;
}
logger.debug("Final SCP mapped path: " + rewrittenPath);
//Create necessary parent directories, if not existing
File rewrittenPathParentDir = new File(rewrittenPath).getParentFile();
rewrittenPathParentDir.mkdirs();
return originalCommandStart + rewrittenPath;
}
private String getScpPathOfCommand() throws AuthenticationException {
// Apache Mina/ SSHD dows not hand over the complete SCP command as entered on Client side.
// the scp command looks like: scp -t (or -r) PFAD
int slashPos = command.indexOf(FORWARD_SLASH);
if (slashPos == NOT_FOUND_INDEX) {
throw new AuthenticationException("SCP path must contain at least one forward slash");
}
if (command.charAt(slashPos - 1) != ' ') {
throw new AuthenticationException("SCP path must start with a forward slash");
}
String path = command.substring(slashPos).trim();
if (path.contains("..") || path.contains("..")) {
throw new AuthenticationException("Parent folder traversal (\"..\") is disallowed");
}
return path;
}
@Override
public void setInputStream(InputStream inParam) {
this.in = inParam;
}
@Override
public void setOutputStream(OutputStream outParam) {
this.out = outParam;
}
@Override
public void setErrorStream(OutputStream errParam) {
this.err = errParam;
}
@Override
public void setExitCallback(ExitCallback callbackParam) {
this.callback = callbackParam;
}
@Override
// TODO review: use this for additional security? - misc_ro
public void setFileSystem(FileSystem fileSystemParam) {
this.fileSystem = fileSystemParam;
}
@Override
public void setSession(ServerSession arg0) {
this.session = arg0;
}
}