Skip to content

Commit

Permalink
增加crash,用于实现web console,并实现了websocket 的权限控制
Browse files Browse the repository at this point in the history
  • Loading branch information
hengyunabc committed Aug 15, 2015
1 parent db4eaed commit 6b90e54
Show file tree
Hide file tree
Showing 46 changed files with 11,044 additions and 34 deletions.
3 changes: 2 additions & 1 deletion xdiamond-server/bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"angularjs-toaster": "0.4.14",
"angular-elastic": "2.4.2",
"ui-router-extras": "0.0.14",
"echarts": "~2.2.7"
"echarts": "2.2.7",
"jquery.terminal": "0.8.8"
},
"devDependencies": {
"angular-mocks": "1.3.16"
Expand Down
46 changes: 43 additions & 3 deletions xdiamond-server/pom.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<?xml version="1.0"?>
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>

<parent>
Expand Down Expand Up @@ -36,6 +35,36 @@
<version>2.0.2.RELEASE</version>
</dependency>

<!-- crash -->
<dependency>
<groupId>org.crashub</groupId>
<artifactId>crash.embed.spring</artifactId>
<version>1.3.1</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.3.1</version>
</dependency>

<!-- Conversion -->
<dependency>
<groupId>net.sf.dozer</groupId>
Expand Down Expand Up @@ -339,6 +368,12 @@
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
Expand All @@ -361,6 +396,11 @@
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.5</version>
</dependency>

<!-- test -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.github.xdiamond.web.api.controller;

import java.util.concurrent.TimeUnit;

import io.github.xdiamond.web.RestResult;
import io.github.xdiamond.web.shiro.PermissionHelper;

import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.codahale.metrics.annotation.Timed;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

@Controller
@RequestMapping(value = "/api")
public class CrashController {

static Cache<Object, Object> crashTokenCache = CacheBuilder.newBuilder()
.expireAfterWrite(30 * 1000, TimeUnit.MILLISECONDS).maximumSize(1000).build();

/**
* 获取crash websocket的访问token
*
* @return
*/
@RequestMapping(value = "/crash/token", method = RequestMethod.GET)
@Timed
public ResponseEntity<RestResult> token() {
PermissionHelper.checkAdmin();

String token = RandomStringUtils.randomAlphanumeric(16);
crashTokenCache.put(token, new Object());
return RestResult.success().withResult("token", token).build();
}

/**
* 检查token是否存在,如果存在,即用这个token来连接的websocket的连接是合法的
*
* @param token
* @return
*/
static public boolean checkToken(String token) {
return crashTokenCache.getIfPresent(token) != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*
* 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 io.github.xdiamond.web.crash;

import io.github.xdiamond.web.api.controller.CrashController;

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;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

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 com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

/**
* @author Julien Viet
* @author hengyunabc
*
* 增加Token权限检验功能
* */
@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) {
// 检查token是否合法
String token = null;
List<String> tokenList = wsSession.getRequestParameterMap().get("token");
if (tokenList != null && !tokenList.isEmpty()) {
token = tokenList.get(0);
}
if (token == null || !CrashController.checkToken(token)) {
throw new RuntimeException("token is invalid");
}

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);
enabled = true;
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);
}
}
}
Loading

0 comments on commit 6b90e54

Please sign in to comment.