/** * 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.io.InputStream; import java.io.OutputStream; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.apache.sshd.server.Command; import org.apache.sshd.server.Environment; import org.apache.sshd.server.ExitCallback; import org.apache.sshd.server.Signal; import org.apache.sshd.server.SignalListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.structr.api.config.Settings; import org.structr.common.AccessMode; import org.structr.common.Permission; import org.structr.common.SecurityContext; import org.structr.common.error.FrameworkException; import org.structr.core.app.App; import org.structr.core.app.StructrApp; import org.structr.core.graph.Tx; import org.structr.files.ssh.shell.CatCommand; import org.structr.files.ssh.shell.CdCommand; import org.structr.files.ssh.shell.LogoutCommand; import org.structr.files.ssh.shell.LsCommand; import org.structr.files.ssh.shell.MkdirCommand; import org.structr.files.ssh.shell.PasswordCommand; import org.structr.files.ssh.shell.ShellCommand; import org.structr.web.entity.AbstractFile; import org.structr.web.entity.Folder; import org.structr.web.entity.User; /** * * */ public class StructrShellCommand implements Command, SignalListener, TerminalHandler { private static final Logger logger = LoggerFactory.getLogger(StructrShellCommand.class.getName()); private static final Map<String, Class<? extends ShellCommand>> commands = new LinkedHashMap<>(); static { commands.put("cat", CatCommand.class); commands.put("cd", CdCommand.class); commands.put("exit", LogoutCommand.class); commands.put("logout", LogoutCommand.class); commands.put("ls", LsCommand.class); commands.put("mkdir", MkdirCommand.class); commands.put("passwd", PasswordCommand.class); } private final List<String> commandHistory = new LinkedList<>(); private Folder currentFolder = null; private TerminalEmulator term = null; private ExitCallback callback = null; private InputStream in = null; private OutputStream out = null; private OutputStream err = null; private User user = null; @Override public void setInputStream(final InputStream in) { this.in = in; } @Override public void setOutputStream(final OutputStream out) { this.out = out; } @Override public void setErrorStream(final OutputStream err) { this.err = err; } @Override public void setExitCallback(final ExitCallback callback) { this.callback = callback; } @Override public void start(final Environment env) throws IOException { env.addSignalListener(this); final String userName = env.getEnv().get("USER"); if (userName != null) { final App app = StructrApp.getInstance(); try (final Tx tx = app.tx()) { user = app.nodeQuery(User.class).andName(userName).getFirst(); if (user != null) { // set home directory first if (Settings.FilesystemEnabled.getValue()) { currentFolder = user.getProperty(User.homeDirectory); } } tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); } } else { logger.warn("Cannot start Structr shell, no username set!"); return; } if (isInteractive()) { // abort if no user was found if (user == null) { logger.warn("Cannot start Structr shell, user not found for name {}!", userName); return; } final String terminalType = env.getEnv().get("TERM"); if (terminalType != null) { switch (terminalType) { case "xterm": case "vt100": case "vt220": term = new XTermTerminalEmulator(in, out, this); break; default: logger.warn("Unsupported terminal type {}, aborting.", terminalType); break; } logger.warn("No terminal type provided, aborting.", terminalType); } if (term != null) { term.start(); term.print("Welcome to "); term.setBold(true); term.print("Structr"); term.print(" 2.0"); term.setBold(false); term.println(); // display first prompt displayPrompt(); } else { callback.onExit(1); } } else { // create terminal emulation term = new XTermTerminalEmulator(in, out, this); } } @Override public void destroy() { if (term != null) { term.stopEmulator(); } } @Override public void signal(final Signal signal) { logger.info("Received signal {}", signal.name()); } @Override public void handleLine(final String line) throws IOException { if (StringUtils.isNotBlank(line)) { final ShellCommand cmd = getCommandForLine(line); if (cmd != null) { cmd.setCommand(line); cmd.setUser(user); cmd.setTerminalEmulator(term); cmd.execute(this); } commandHistory.add(line); } } @Override public void handleExit() { if (callback != null) { callback.onExit(0); } } public String getPrompt() { final App app = StructrApp.getInstance(); final StringBuilder buf = new StringBuilder(); try (final Tx tx = app.tx()) { buf.append(user.getName()); buf.append("@structr:"); if (currentFolder != null) { String folderPart = currentFolder.getProperty(AbstractFile.path); final Folder homeFolder = user.getProperty(User.homeDirectory); if (homeFolder != null) { // replace home directory with ~ if at the beginning of the full path final String homeFolderPath = homeFolder.getProperty(AbstractFile.path); if (folderPart.startsWith(homeFolderPath)) { folderPart = "~" + folderPart.substring(homeFolderPath.length()); } } buf.append(folderPart); } else { buf.append("/"); } buf.append(user.isAdmin() ? "#" : "$"); buf.append(" "); tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); } return buf.toString(); } public Folder getCurrentFolder() { return currentFolder; } public void setCurrentFolder(final Folder folder) { this.currentFolder = folder; } public Folder findRelativeFolder(final Folder baseFolder, final String path) throws FrameworkException { final App app = StructrApp.getInstance(); Folder folder = baseFolder; boolean found = false; for (final String part : path.split("[/]+")) { if (folder == null) { folder = app.nodeQuery(Folder.class).and(Folder.name, part).getFirst(); } else { for (final Folder child : folder.getProperty(Folder.folders)) { if (part.equals(child.getName())) { folder = child; found = true; } } if (!found) { return null; } } } return folder; } public boolean isAllowed(final AbstractFile file, final Permission permission, final boolean explicit) { if (file == null) { return false; } final SecurityContext securityContext = SecurityContext.getInstance(user, AccessMode.Backend); if (Permission.read.equals(permission) && !explicit) { return file.isVisibleToAuthenticatedUsers() || file.isVisibleToPublicUsers() || file.isGranted(permission, securityContext); } return file.isGranted(permission, securityContext); } // ----- private methods ----- private ShellCommand getCommandForLine(final String line) { final int pos = line.indexOf(" "); final String commandString = pos > 0 ? line.substring(0, pos) : line; final Class<? extends ShellCommand> clazz = commands.get(commandString); if (clazz != null) { try { return clazz.newInstance(); } catch (Throwable t) { logger.warn("", t); } } return null; } @Override public List<String> getCommandHistory() { return commandHistory; } @Override public void displayPrompt() throws IOException { // output prompt term.print(getPrompt()); } @Override public void handleLogoutRequest() throws IOException { // Ctrl-D is logout term.println("logout"); term.stopEmulator(); } @Override public void handleCtrlC() throws IOException { // Ctrl-C term.print("^C"); term.clearLineBuffer(); term.handleNewline(); } @Override public void setUser(final User user) { this.user = user; } @Override public User getUser() { return user; } @Override public void handleShiftTab() throws IOException { } @Override public void handleTab(final int tabCount) throws IOException { final String line = term.getLineBuffer().toString(); if (StringUtils.isNotBlank(line)) { final ShellCommand cmd = getCommandForLine(line); if (cmd != null) { cmd.setUser(user); cmd.setTerminalEmulator(term); cmd.handleTabCompletion(this, line, tabCount); } } } public boolean isInteractive() { return true; } public void flush() throws IOException { if (term != null) { term.flush(); } } }