/**
* Copyright (C) 2010-2017 Structr GmbH
*
* This file is part of Structr <http://structr.org>.
*
* Structr is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Structr is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Structr. If not, see <http://www.gnu.org/licenses/>.
*/
package org.structr.files.ssh;
import java.io.IOException;
import java.nio.file.CopyOption;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.PublicKey;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.sshd.common.Factory;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.PublicKeyEntry;
import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
import org.apache.sshd.common.file.FileSystemFactory;
import org.apache.sshd.common.session.Session;
import org.apache.sshd.server.CommandFactory;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.scp.ScpCommandFactory;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.subsystem.sftp.DirectoryHandle;
import org.apache.sshd.server.subsystem.sftp.FileHandle;
import org.apache.sshd.server.subsystem.sftp.Handle;
import org.apache.sshd.server.subsystem.sftp.SftpEventListener;
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.api.config.Settings;
import org.structr.api.service.Command;
import org.structr.api.service.SingletonService;
import org.structr.api.service.StructrServices;
import org.structr.common.AccessMode;
import org.structr.common.SecurityContext;
import org.structr.console.Console.ConsoleMode;
import org.structr.core.app.StructrApp;
import org.structr.core.entity.AbstractNode;
import org.structr.core.entity.Principal;
import org.structr.core.graph.Tx;
import org.structr.files.ssh.filesystem.StructrFilesystem;
import org.structr.rest.auth.AuthHelper;
/**
*
*
*/
public class SSHService implements SingletonService, PasswordAuthenticator, PublickeyAuthenticator, FileSystemFactory, Factory<org.apache.sshd.server.Command>, SftpEventListener, CommandFactory {
private static final Logger logger = LoggerFactory.getLogger(SSHService.class.getName());
private final ScpCommandFactory scp = new ScpCommandFactory.Builder().build();
private SshServer server = null;
private boolean running = false;
private SecurityContext securityContext = null;
@Override
public void injectArguments(final Command command) {
}
@Override
public void initialize(final StructrServices services) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
logger.info("Setting up SSH server..");
server = SshServer.setUpDefaultServer();
logger.info("Initializing host key generator..");
final SimpleGeneratorHostKeyProvider hostKeyProvider = new SimpleGeneratorHostKeyProvider(Paths.get("db/structr_hostkey"));
hostKeyProvider.setAlgorithm(KeyUtils.RSA_ALGORITHM);
logger.info("Configuring SSH server..");
server.setKeyPairProvider(hostKeyProvider);
server.setPort(Settings.SshPort.getValue());
server.setPasswordAuthenticator(this);
server.setPublickeyAuthenticator(this);
server.setFileSystemFactory(this);
server.setSubsystemFactories(getSubsystems());
server.setShellFactory(this);
server.setCommandFactory(this);
logger.info("Starting SSH server..");
try {
server.start();
running = true;
} catch (IOException ex) {
ex.printStackTrace();
//logger.error("", ex);
}
logger.info("Initialization complete.");
}
@Override
public void shutdown() {
try {
server.stop(true);
running = false;
} catch (IOException ex) {
logger.error("", ex);
}
}
@Override
public void initialized() {
}
@Override
public String getName() {
return "SSHService";
}
@Override
public boolean isRunning() {
return server != null && running;
}
@Override
public boolean isVital() {
return false;
}
@Override
public FileSystem createFileSystem(final Session session) throws IOException {
//return new StructrSSHFileSystem(securityContext, session);
return new StructrFilesystem(securityContext);
}
@Override
public boolean authenticate(final String username, final String password, final ServerSession session) {
boolean isValid = false;
Principal principal = null;
try (final Tx tx = StructrApp.getInstance().tx()) {
principal = AuthHelper.getPrincipalForPassword(AbstractNode.name, username, password);
if (principal != null) {
isValid = true;
securityContext = SecurityContext.getInstance(principal, AccessMode.Backend);
}
tx.success();
} catch (Throwable t) {
logger.warn("", t);
isValid = false;
}
try {
if (isValid) {
session.setAuthenticated();
}
} catch (IOException ex) {
logger.error("", ex);
}
return isValid;
}
@Override
public boolean authenticate(final String username, final PublicKey key, final ServerSession session) {
boolean isValid = false;
if (key == null) {
return isValid;
}
try (final Tx tx = StructrApp.getInstance().tx()) {
final Principal principal = StructrApp.getInstance().nodeQuery(Principal.class).andName(username).getFirst();
if (principal != null) {
securityContext = SecurityContext.getInstance(principal, AccessMode.Backend);
// check single (main) pubkey
final String pubKeyData = principal.getProperty(Principal.publicKey);
if (pubKeyData != null) {
final PublicKey pubKey = PublicKeyEntry.parsePublicKeyEntry(pubKeyData).resolvePublicKey(PublicKeyEntryResolver.FAILING);
isValid = KeyUtils.compareKeys(pubKey, key);
}
// check array of pubkeys for this user
final String[] pubKeysData = principal.getProperty(Principal.publicKeys);
if (pubKeysData != null) {
for (final String k : pubKeysData) {
if (k != null) {
final PublicKey pubKey = PublicKeyEntry.parsePublicKeyEntry(k).resolvePublicKey(PublicKeyEntryResolver.FAILING);
if (KeyUtils.compareKeys(pubKey, key)) {
isValid = true;
break;
}
}
}
}
}
tx.success();
} catch (Throwable t) {
logger.warn("", t);
isValid = false;
}
try {
if (isValid) {
session.setAuthenticated();
}
} catch (IOException ex) {
logger.error("Unable to authenticate session", ex);
}
return isValid;
}
private Tx currentTransaction = null;
@Override
public org.apache.sshd.server.Command create() {
return new StructrConsoleCommand(securityContext);
}
// ----- private methods -----
private void beginTransaction() {
if (currentTransaction == null) {
currentTransaction = StructrApp.getInstance(securityContext).tx(true, false, false);
}
}
private void endTransaction() {
if (currentTransaction != null) {
try {
currentTransaction.success();
currentTransaction.close();
} catch (Throwable t) {
logger.warn("", t);
} finally {
currentTransaction = null;
}
}
}
// ----- interface SftpEventListener -----
@Override
public void initialized(ServerSession session, int version) {
}
@Override
public void destroying(ServerSession session) {
}
@Override
public void open(ServerSession session, String remoteHandle, Handle localHandle) {
beginTransaction();
}
@Override
public void read(ServerSession session, String remoteHandle, DirectoryHandle localHandle, Map<String, Path> entries) {
}
@Override
public void read(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, byte[] data, int dataOffset, int dataLen, int readLen) {
}
@Override
public void write(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, byte[] data, int dataOffset, int dataLen) {
}
@Override
public void blocking(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length, int mask) {
}
@Override
public void blocked(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length, int mask, Throwable thrown) {
}
@Override
public void unblocking(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length) {
}
@Override
public void unblocked(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length, Boolean result, Throwable thrown) {
}
@Override
public void close(ServerSession session, String remoteHandle, Handle localHandle) {
endTransaction();
}
@Override
public void creating(ServerSession session, Path path, Map<String, ?> attrs) {
beginTransaction();
}
@Override
public void created(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown) {
endTransaction();
}
@Override
public void moving(ServerSession session, Path srcPath, Path dstPath, Collection<CopyOption> opts) {
beginTransaction();
}
@Override
public void moved(ServerSession session, Path srcPath, Path dstPath, Collection<CopyOption> opts, Throwable thrown) {
endTransaction();
}
@Override
public void removing(ServerSession session, Path path) {
beginTransaction();
}
@Override
public void removed(ServerSession session, Path path, Throwable thrown) {
endTransaction();
}
@Override
public void linking(ServerSession session, Path source, Path target, boolean symLink) {
beginTransaction();
}
@Override
public void linked(ServerSession session, Path source, Path target, boolean symLink, Throwable thrown) {
endTransaction();
}
@Override
public void modifyingAttributes(ServerSession session, Path path, Map<String, ?> attrs) {
}
@Override
public void modifiedAttributes(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown) {
}
// ----- interface CommandFactory -----
@Override
public org.apache.sshd.server.Command createCommand(final String command) {
if (command.startsWith("scp ")) {
return scp.createCommand(command);
}
if (command.startsWith("javascript ")) {
return new StructrConsoleCommand(securityContext, ConsoleMode.JavaScript, command.substring(11));
}
if (command.startsWith("structrscript ")) {
return new StructrConsoleCommand(securityContext, ConsoleMode.StructrScript, command.substring(14));
}
if (command.startsWith("cypher ")) {
return new StructrConsoleCommand(securityContext, ConsoleMode.Cypher, command.substring(7));
}
if (command.startsWith("admin ")) {
return new StructrConsoleCommand(securityContext, ConsoleMode.AdminShell, command.substring(6));
}
if (command.startsWith("rest ")) {
return new StructrConsoleCommand(securityContext, ConsoleMode.REST, command.substring(5));
}
throw new IllegalStateException("Unknown subsystem for command '" + command + "'");
}
// ----- private methods -----
private List<NamedFactory<org.apache.sshd.server.Command>> getSubsystems() {
final List<NamedFactory<org.apache.sshd.server.Command>> list = new LinkedList<>();
// sftp
final SftpSubsystemFactory factory = new SftpSubsystemFactory();
list.add(factory);
factory.addSftpEventListener(this);
return list;
}
// ----- nested classes -----
private class ConsoleCommandFactory implements NamedFactory<org.apache.sshd.server.Command> {
private ConsoleMode mode = null;
public ConsoleCommandFactory(final ConsoleMode mode) {
this.mode = mode;
}
@Override
public org.apache.sshd.server.Command create() {
return new StructrConsoleCommand(securityContext);
}
@Override
public String getName() {
return mode.name();
}
}
}