/** * 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.io.OutputStreamWriter; import java.io.Writer; import java.util.Iterator; import java.util.LinkedList; import java.util.List; 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.common.AccessMode; import org.structr.common.Permission; import org.structr.common.SecurityContext; import org.structr.common.error.FrameworkException; import org.structr.console.Console; import org.structr.console.Console.ConsoleMode; import org.structr.console.tabcompletion.TabCompletionResult; import org.structr.core.app.App; import org.structr.core.app.StructrApp; import org.structr.core.graph.Tx; import org.structr.util.Writable; import org.structr.web.entity.AbstractFile; import org.structr.web.entity.User; /** * */ public class StructrConsoleCommand implements Command, SignalListener, TerminalHandler { private static final Logger logger = LoggerFactory.getLogger(StructrConsoleCommand.class.getName()); private final List<String> commandHistory = new LinkedList<>(); private final StringBuilder lastBlockChars = new StringBuilder(); private final StringBuilder buf = new StringBuilder(); private ConsoleMode consoleMode = ConsoleMode.JavaScript; private String command = null; private Console console = null; private TerminalEmulator term = null; private ExitCallback callback = null; private InputStream in = null; private OutputStream out = null; private User user = null; private int inBlock = 0; private int inSingleQuotes = 0; private int inDoubleQuotes = 0; private int inArray = 0; private int inBraces = 0; public StructrConsoleCommand(final SecurityContext securityContext) { this(securityContext, ConsoleMode.JavaScript, null); } public StructrConsoleCommand(final SecurityContext securityContext, final ConsoleMode consoleMode, final String command) { this.console = new Console(securityContext, consoleMode, null); this.consoleMode = consoleMode; this.command = command; } @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) { } @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(); tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); } } else { logger.warn("Cannot start Structr shell, no username set!"); return; } // 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) { if (terminalType.startsWith("xterm") || terminalType.startsWith("vt100") || terminalType.startsWith("vt220")) { term = new XTermTerminalEmulator(in, out, this); } else { logger.warn("Unsupported terminal type {}, aborting.", terminalType); } logger.warn("No terminal type provided, aborting.", terminalType); } if (term != null) { term.start(); term.print("Welcome to the "); term.setBold(true); term.print("Structr 2.1"); term.setBold(false); term.print(" JavaScript console. Use <Shift>+<Tab> to switch modes."); term.println(); // display first prompt displayPrompt(); } else { final OutputStreamWritable writable = new OutputStreamWritable(out); if (command != null) { try { this.console.run(command, writable); } catch (FrameworkException fex) { writable.println(fex.getMessage()); } } else { writable.println("No command specified, aborting."); } callback.onExit(1); } } @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 { try { term.flush(); if (StringUtils.isNotBlank(line)) { if (insideOfBlockOrStructure()) { buf.append(line); buf.append("\r\n"); } else { buf.append(line); } if ("exit".equals(line) || "quit".equals(line)) { term.stopEmulator(); } else { checkForBlockChars(line.trim()); if (!insideOfBlockOrStructure()) { final String command = buf.toString(); clearBlockStatus(); commandHistory.add(command); try (final Tx tx = StructrApp.getInstance(console.getSecurityContext()).tx()) { console.run(command, term); tx.success(); } catch (Throwable t) { final String message = t.getMessage(); if (message != null) { term.println(message); } else { logger.warn("", t); term.println(t.getClass().getSimpleName() + " encountered."); } } } } } } catch (Throwable t) { logger.warn("", t); term.println(t.getClass().getSimpleName() + " encountered."); } finally { term.flush(); } } @Override public void handleExit() { if (callback != null) { callback.onExit(0); } } public String getPrompt() { final StringBuilder buffer = new StringBuilder(); buffer.append("\u001b[1m"); buffer.append(console.getPrompt()); if (insideOfBlockOrStructure() && lastBlockChars.length() > 0) { buffer.append(lastBlockChars.charAt(lastBlockChars.length() - 1)); } else { buffer.append("/"); } buffer.append(">"); buffer.append("\u001b[0m"); buffer.append(" "); return buffer.toString(); } 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 ----- @Override public List<String> getCommandHistory() { return commandHistory; } @Override public void displayPrompt() throws IOException { // output prompt term.setBold(true); term.setTextColor(7); term.print(getPrompt()); } @Override public void handleLogoutRequest() throws IOException { // Ctrl-D is logout term.println("logout"); term.stopEmulator(); } @Override public void handleCtrlC() throws IOException { clearBlockStatus(); // 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 { if (!insideOfBlockOrStructure()) { switch (consoleMode) { case REST: consoleMode = ConsoleMode.JavaScript; break; case JavaScript: consoleMode = ConsoleMode.StructrScript; break; case StructrScript: consoleMode = ConsoleMode.Cypher; break; case Cypher: consoleMode = ConsoleMode.AdminShell; break; case AdminShell: consoleMode = ConsoleMode.REST; break; } term.handleString("Console.setMode('" + consoleMode.name() + "')"); term.clearTabCount(); term.setBold(true); term.setTextColor(3); term.handleNewline(); } } @Override public void handleTab(final int tabCount) throws IOException { if (!insideOfBlockOrStructure()) { final StringBuilder lineBuffer = term.getLineBuffer(); final List<TabCompletionResult> tabCompletion = console.getTabCompletion(lineBuffer.toString()); if (!tabCompletion.isEmpty()) { // exactly one result => success if (tabCompletion.size() == 1) { final TabCompletionResult result = tabCompletion.iterator().next(); term.handleString(result.getCompletion()); term.handleString(result.getSuffix()); term.clearTabCount(); } else { if (tabCount > 1) { // display alternatives term.println(); for (final Iterator<TabCompletionResult> it = tabCompletion.iterator(); it.hasNext();) { final TabCompletionResult result = it.next(); term.print(result.getCommand()); if (it.hasNext()) { term.print(" "); } } term.println(); term.print(getPrompt()); term.print(term.getLineBuffer().toString()); } } } } } public void flush() throws IOException { if (term != null) { term.flush(); } } // ----- private method ----- private boolean insideOfBlockOrStructure() { final boolean singleQuotes = (inSingleQuotes % 2) != 0; final boolean doubleQuotes = (inDoubleQuotes % 2) != 0; return inBlock > 0 || doubleQuotes || singleQuotes || inArray > 0 || inBraces > 0; } private void checkForBlockChars(final String line) { try { for (final char c : line.toCharArray()) { switch (c) { case '{': lastBlockChars.append("{"); inBlock++; break; case '}': lastBlockChars.setLength(Math.max(0, lastBlockChars.length() - 1)); inBlock--; break; case '[': lastBlockChars.append("["); inArray++; break; case ']': lastBlockChars.setLength(Math.max(0, lastBlockChars.length() - 1)); inArray--; break; case '(': lastBlockChars.append("("); inBraces++; break; case ')': lastBlockChars.setLength(Math.max(0, lastBlockChars.length() - 1)); inBraces--; break; case '"': inDoubleQuotes++; if ((inDoubleQuotes % 2) == 0) { lastBlockChars.setLength(Math.max(0, lastBlockChars.length() - 1)); } else { lastBlockChars.append("\""); } break; case '\'': inSingleQuotes++; if ((inSingleQuotes % 2) == 0) { lastBlockChars.setLength(Math.max(0, lastBlockChars.length() - 1)); } else { lastBlockChars.append("'"); } break; } } } catch (Throwable t) {} } private void clearBlockStatus() { inSingleQuotes = 0; inDoubleQuotes = 0; inBraces = 0; inBlock = 0; inArray = 0; buf.setLength(0); } // ----- nested classes ----- private static class OutputStreamWritable implements Writable { private Writer writer = null; public OutputStreamWritable(final OutputStream out) { this.writer = new OutputStreamWriter(out); } @Override public void print(final Object... text) throws IOException { if (text != null) { for (final Object o : text) { if (o != null) { writer.write(o.toString().replaceAll("\n", "\r\n")); } else { writer.write("null"); } } } writer.flush(); } @Override public void println(final Object... text) throws IOException { print(text); println(); writer.flush(); } @Override public void println() throws IOException { writer.write(10); writer.write(13); } @Override public void flush() throws IOException { writer.flush(); } } }