/*
* Copyright (C) 2012 eXo Platform SAS.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.crsh.web.servlet;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.crsh.cli.impl.Delimiter;
import org.crsh.cli.impl.completion.CompletionMatch;
import org.crsh.cli.spi.Completion;
import org.crsh.keyboard.KeyType;
import org.crsh.plugin.PluginContext;
import org.crsh.plugin.WebPluginLifeCycle;
import org.crsh.shell.Shell;
import org.crsh.shell.ShellFactory;
import org.crsh.shell.ShellProcess;
import org.crsh.util.Utils;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.net.URI;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
/** @author Julien Viet */
@ServerEndpoint(value = "/crash", configurator = Configurator.class)
public class CRaSHConnector {
/** . */
static final Logger log = Logger.getLogger(CRaSHConnector.class.getName());
/** . */
private final ConcurrentHashMap<String, CRaSHSession> sessions = new ConcurrentHashMap<String, CRaSHSession>();
/** . */
private static final ThreadLocal<Session> current = new ThreadLocal<Session>();
/**
* @return the current session crash id (CRASHID) or null if none is associated with the request
*/
public static String getHttpSessionId() {
Session session = current.get();
if (session != null) {
return (String)session.getUserProperties().get("CRASHID");
} else {
return null;
}
}
@OnOpen
public void start(Session wsSession) {
current.set(wsSession);
try {
URI uri = wsSession.getRequestURI();
String path = uri.getPath();
log.fine("Establishing session for " + path);
String contextPath = path.substring(0, path.lastIndexOf('/'));
PluginContext context = WebPluginLifeCycle.getPluginContext(contextPath);
if (context != null) {
Boolean enabled = context.getProperty(WebPlugin.ENABLED);
if (enabled != null && enabled) {
log.fine("Using shell " + context);
ShellFactory factory = context.getPlugin(ShellFactory.class);
Principal user = wsSession.getUserPrincipal();
Shell shell = factory.create(user);
CRaSHSession session = new CRaSHSession(wsSession, shell);
sessions.put(wsSession.getId(), session);
log.fine("Established session " + wsSession.getId());
} else {
log.fine("Web plugin disabled");
}
} else {
log.fine("No shell found");
}
}
finally {
current.set(null);
}
}
@OnClose
public void end(Session wsSession) {
current.set(wsSession);
try {
CRaSHSession session = sessions.remove(wsSession.getId());
if (session != null) {
log.fine("Destroying session " + wsSession.getId());
WSProcessContext current = session.current.getAndSet(null);
if (current != null) {
log.fine("Cancelling on going command " + current.command + " for " + wsSession.getId());
current.process.cancel();
}
} else {
log.fine("No shell session found");
}
}
finally {
current.set(null);
}
}
@OnMessage
public void incoming(String message, Session wsSession) {
String key = wsSession.getId();
log.fine("Received message " + message + " from session " + key);
current.set(wsSession);
try {
CRaSHSession session = sessions.get(key);
if (session != null) {
JsonParser parser = new JsonParser();
JsonElement json = parser.parse(message);
if (json instanceof JsonObject) {
JsonObject event = (JsonObject)json;
JsonElement type = event.get("type");
if (type.getAsString().equals("welcome")) {
log.fine("Sending welcome + prompt");
session.send("print", session.shell.getWelcome());
session.send("prompt", session.shell.getPrompt());
} else if (type.getAsString().equals("execute")) {
String command = event.get("command").getAsString();
int width = event.get("width").getAsInt();
int height = event.get("height").getAsInt();
ShellProcess process = session.shell.createProcess(command);
WSProcessContext context = new WSProcessContext(session, process, command, width, height);
if (session.current.getAndSet(context) == null) {
log.fine("Executing \"" + command + "\"");
process.execute(context);
} else {
log.fine("Could not execute \"" + command + "\"");
}
} else if (type.getAsString().equals("cancel")) {
WSProcessContext current = session.current.getAndSet(null);
if (current != null) {
log.fine("Cancelling command \"" + current.command + "\"");
current.process.cancel();
} else {
log.fine("No process to cancel");
}
} else if (type.getAsString().equals("key")) {
WSProcessContext current = session.current.get();
if (current != null) {
String _keyType = event.get("keyType").getAsString();
KeyType keyType = KeyType.valueOf(_keyType.toUpperCase());
if (keyType == KeyType.CHARACTER) {
int code = event.get("keyCode").getAsInt();
if (code >= 32) {
current.handle(KeyType.CHARACTER, new int[]{code});
}
} else {
current.handle(keyType, new int[0]);
}
} else {
log.fine("No process can handle the key event");
}
} else if (type.getAsString().equals("complete")) {
String prefix = event.get("prefix").getAsString();
CompletionMatch completion = session.shell.complete(prefix);
Completion completions = completion.getValue();
Delimiter delimiter = completion.getDelimiter();
StringBuilder sb = new StringBuilder();
List<String> values = new ArrayList<String>();
try {
if (completions.getSize() == 1) {
String value = completions.getValues().iterator().next();
delimiter.escape(value, sb);
if (completions.get(value)) {
sb.append(delimiter.getValue());
}
values.add(sb.toString());
}
else {
String commonCompletion = Utils.findLongestCommonPrefix(completions.getValues());
if (commonCompletion.length() > 0) {
delimiter.escape(commonCompletion, sb);
values.add(sb.toString());
}
else {
for (Map.Entry<String, Boolean> entry : completions) {
delimiter.escape(entry.getKey(), sb);
values.add(sb.toString());
sb.setLength(0);
}
}
}
}
catch (IOException ignore) {
// Should not happen
}
log.fine("Completing \"" + prefix + "\" with " + values);
session.send("complete", values);
}
}
} else {
log.fine("No shell session found");
}
}
finally {
current.set(null);
}
}
}