blob: 2753d9fcb6541a7b0674ab5bb8963baca11c75e5 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.consoleproxy;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.xml.DOMConfigurator;
import org.eclipse.jetty.websocket.api.Session;
import com.cloud.consoleproxy.util.Logger;
import com.cloud.utils.PropertiesUtil;
import com.google.gson.Gson;
import com.sun.net.httpserver.HttpServer;
/**
*
* ConsoleProxy, singleton class that manages overall activities in console proxy process. To make legacy code work, we still
*/
public class ConsoleProxy {
private static final Logger s_logger = Logger.getLogger(ConsoleProxy.class);
public static final int KEYBOARD_RAW = 0;
public static final int KEYBOARD_COOKED = 1;
public static final int VIEWER_LINGER_SECONDS = 180;
public static Object context;
// this has become more ugly, to store keystore info passed from management server (we now use management server managed keystore to support
// dynamically changing to customer supplied certificate)
public static byte[] ksBits;
public static String ksPassword;
public static Boolean isSourceIpCheckEnabled;
public static Method authMethod;
public static Method reportMethod;
public static Method ensureRouteMethod;
static Hashtable<String, ConsoleProxyClient> connectionMap = new Hashtable<String, ConsoleProxyClient>();
static Set<String> removedSessionsSet = ConcurrentHashMap.newKeySet();
static int httpListenPort = 80;
static int httpCmdListenPort = 8001;
static int reconnectMaxRetry = 5;
static int readTimeoutSeconds = 90;
static int keyboardType = KEYBOARD_RAW;
static String factoryClzName;
static boolean standaloneStart = false;
static String encryptorPassword = "Dummy";
static final String[] skipProperties = new String[]{"certificate", "cacertificate", "keystore_password", "privatekey"};
static Set<String> allowedSessions = new HashSet<>();
public static void addAllowedSession(String sessionUuid) {
allowedSessions.add(sessionUuid);
}
private static void configLog4j() {
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
URL configUrl = loader.getResource("/conf/log4j-cloud.xml");
if (configUrl == null)
configUrl = ClassLoader.getSystemResource("log4j-cloud.xml");
if (configUrl == null)
configUrl = ClassLoader.getSystemResource("conf/log4j-cloud.xml");
if (configUrl != null) {
try {
System.out.println("Configure log4j using " + configUrl.toURI().toString());
} catch (URISyntaxException e1) {
e1.printStackTrace();
}
try {
File file = new File(configUrl.toURI());
System.out.println("Log4j configuration from : " + file.getAbsolutePath());
DOMConfigurator.configureAndWatch(file.getAbsolutePath(), 10000);
} catch (URISyntaxException e) {
System.out.println("Unable to convert log4j configuration Url to URI");
}
// DOMConfigurator.configure(configUrl);
} else {
System.out.println("Configure log4j with default properties");
}
}
private static void configProxy(Properties conf) {
s_logger.info("Configure console proxy...");
for (Object key : conf.keySet()) {
s_logger.info("Property " + (String)key + ": " + conf.getProperty((String)key));
if (!ArrayUtils.contains(skipProperties, key)) {
s_logger.info("Property " + (String)key + ": " + conf.getProperty((String)key));
}
}
String s = conf.getProperty("consoleproxy.httpListenPort");
if (s != null) {
httpListenPort = Integer.parseInt(s);
s_logger.info("Setting httpListenPort=" + s);
}
s = conf.getProperty("premium");
if (s != null && s.equalsIgnoreCase("true")) {
s_logger.info("Premium setting will override settings from consoleproxy.properties, listen at port 443");
httpListenPort = 443;
factoryClzName = "com.cloud.consoleproxy.ConsoleProxySecureServerFactoryImpl";
} else {
factoryClzName = ConsoleProxyBaseServerFactoryImpl.class.getName();
}
s = conf.getProperty("consoleproxy.httpCmdListenPort");
if (s != null) {
httpCmdListenPort = Integer.parseInt(s);
s_logger.info("Setting httpCmdListenPort=" + s);
}
s = conf.getProperty("consoleproxy.reconnectMaxRetry");
if (s != null) {
reconnectMaxRetry = Integer.parseInt(s);
s_logger.info("Setting reconnectMaxRetry=" + reconnectMaxRetry);
}
s = conf.getProperty("consoleproxy.readTimeoutSeconds");
if (s != null) {
readTimeoutSeconds = Integer.parseInt(s);
s_logger.info("Setting readTimeoutSeconds=" + readTimeoutSeconds);
}
}
public static ConsoleProxyServerFactory getHttpServerFactory() {
try {
Class<?> clz = Class.forName(factoryClzName);
try {
ConsoleProxyServerFactory factory = (ConsoleProxyServerFactory)clz.newInstance();
factory.init(ConsoleProxy.ksBits, ConsoleProxy.ksPassword);
return factory;
} catch (InstantiationException e) {
s_logger.error(e.getMessage(), e);
return null;
} catch (IllegalAccessException e) {
s_logger.error(e.getMessage(), e);
return null;
}
} catch (ClassNotFoundException e) {
s_logger.warn("Unable to find http server factory class: " + factoryClzName);
return new ConsoleProxyBaseServerFactoryImpl();
}
}
public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(ConsoleProxyClientParam param, boolean reauthentication) {
ConsoleProxyAuthenticationResult authResult = new ConsoleProxyAuthenticationResult();
authResult.setSuccess(true);
authResult.setReauthentication(reauthentication);
authResult.setHost(param.getClientHostAddress());
authResult.setPort(param.getClientHostPort());
if (org.apache.commons.lang3.StringUtils.isNotBlank(param.getExtraSecurityToken())) {
String extraToken = param.getExtraSecurityToken();
String clientProvidedToken = param.getClientProvidedExtraSecurityToken();
s_logger.debug(String.format("Extra security validation for the console access, provided %s " +
"to validate against %s", clientProvidedToken, extraToken));
if (!extraToken.equals(clientProvidedToken)) {
s_logger.error("The provided extra token does not match the expected value for this console endpoint");
authResult.setSuccess(false);
return authResult;
}
}
String sessionUuid = param.getSessionUuid();
if (allowedSessions.contains(sessionUuid)) {
s_logger.debug("Acquiring the session " + sessionUuid + " not available for future use");
allowedSessions.remove(sessionUuid);
} else {
s_logger.info("Session " + sessionUuid + " has already been used, cannot connect");
authResult.setSuccess(false);
return authResult;
}
String websocketUrl = param.getWebsocketUrl();
if (StringUtils.isNotBlank(websocketUrl)) {
return authResult;
}
if (standaloneStart) {
return authResult;
}
if (authMethod != null) {
Object result;
try {
result =
authMethod.invoke(ConsoleProxy.context, param.getClientHostAddress(), String.valueOf(param.getClientHostPort()), param.getClientTag(),
param.getClientHostPassword(), param.getTicket(), reauthentication, param.getSessionUuid());
} catch (IllegalAccessException e) {
s_logger.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException" + " for vm: " + param.getClientTag(), e);
authResult.setSuccess(false);
return authResult;
} catch (InvocationTargetException e) {
s_logger.error("Unable to invoke authenticateConsoleAccess due to InvocationTargetException " + " for vm: " + param.getClientTag(), e);
authResult.setSuccess(false);
return authResult;
}
if (result != null && result instanceof String) {
authResult = new Gson().fromJson((String)result, ConsoleProxyAuthenticationResult.class);
} else {
s_logger.error("Invalid authentication return object " + result + " for vm: " + param.getClientTag() + ", decline the access");
authResult.setSuccess(false);
}
} else {
s_logger.warn("Private channel towards management server is not setup. Switch to offline mode and allow access to vm: " + param.getClientTag());
}
return authResult;
}
public static void reportLoadInfo(String gsonLoadInfo) {
if (reportMethod != null) {
try {
reportMethod.invoke(ConsoleProxy.context, gsonLoadInfo);
} catch (IllegalAccessException e) {
s_logger.error("Unable to invoke reportLoadInfo due to " + e.getMessage());
} catch (InvocationTargetException e) {
s_logger.error("Unable to invoke reportLoadInfo due to " + e.getMessage());
}
} else {
s_logger.warn("Private channel towards management server is not setup. Switch to offline mode and ignore load report");
}
}
public static void ensureRoute(String address) {
if (ensureRouteMethod != null) {
try {
ensureRouteMethod.invoke(ConsoleProxy.context, address);
} catch (IllegalAccessException e) {
s_logger.error("Unable to invoke ensureRoute due to " + e.getMessage());
} catch (InvocationTargetException e) {
s_logger.error("Unable to invoke ensureRoute due to " + e.getMessage());
}
} else {
s_logger.warn("Unable to find ensureRoute method, console proxy agent is not up to date");
}
}
public static void startWithContext(Properties conf, Object context, byte[] ksBits, String ksPassword, String password, Boolean isSourceIpCheckEnabled) {
setEncryptorPassword(password);
configLog4j();
Logger.setFactory(new ConsoleProxyLoggerFactory());
s_logger.info("Start console proxy with context");
if (conf != null) {
for (Object key : conf.keySet()) {
if (!ArrayUtils.contains(skipProperties, key)) {
s_logger.info("Context property " + (String) key + ": " + conf.getProperty((String) key));
}
}
}
// Using reflection to setup private/secure communication channel towards management server
ConsoleProxy.context = context;
ConsoleProxy.ksBits = ksBits;
ConsoleProxy.ksPassword = ksPassword;
ConsoleProxy.isSourceIpCheckEnabled = isSourceIpCheckEnabled;
try {
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> contextClazz = loader.loadClass("com.cloud.agent.resource.consoleproxy.ConsoleProxyResource");
authMethod = contextClazz.getDeclaredMethod("authenticateConsoleAccess", String.class, String.class,
String.class, String.class, String.class, Boolean.class, String.class);
reportMethod = contextClazz.getDeclaredMethod("reportLoadInfo", String.class);
ensureRouteMethod = contextClazz.getDeclaredMethod("ensureRoute", String.class);
} catch (SecurityException e) {
s_logger.error("Unable to setup private channel due to SecurityException", e);
} catch (NoSuchMethodException e) {
s_logger.error("Unable to setup private channel due to NoSuchMethodException", e);
} catch (IllegalArgumentException e) {
s_logger.error("Unable to setup private channel due to IllegalArgumentException", e);
} catch (ClassNotFoundException e) {
s_logger.error("Unable to setup private channel due to ClassNotFoundException", e);
}
// merge properties from conf file
InputStream confs = ConsoleProxy.class.getResourceAsStream("/conf/consoleproxy.properties");
Properties props = new Properties();
if (confs == null) {
final File file = PropertiesUtil.findConfigFile("consoleproxy.properties");
if (file == null)
s_logger.info("Can't load consoleproxy.properties from classpath, will use default configuration");
else
try {
confs = new FileInputStream(file);
} catch (FileNotFoundException e) {
s_logger.info("Ignoring file not found exception and using defaults");
}
}
if (confs != null) {
try {
props.load(confs);
for (Object key : props.keySet()) {
// give properties passed via context high priority, treat properties from consoleproxy.properties
// as default values
if (conf.get(key) == null)
conf.put(key, props.get(key));
}
} catch (Exception e) {
s_logger.error(e.toString(), e);
}
}
try {
confs.close();
} catch (IOException e) {
s_logger.error("Failed to close consolepropxy.properties : " + e.toString(), e);
}
start(conf);
}
public static void start(Properties conf) {
System.setProperty("java.awt.headless", "true");
configProxy(conf);
ConsoleProxyServerFactory factory = getHttpServerFactory();
if (factory == null) {
s_logger.error("Unable to load console proxy server factory");
System.exit(1);
}
if (httpListenPort != 0) {
startupHttpMain();
} else {
s_logger.error("A valid HTTP server port is required to be specified, please check your consoleproxy.httpListenPort settings");
System.exit(1);
}
if (httpCmdListenPort > 0) {
startupHttpCmdPort();
} else {
s_logger.info("HTTP command port is disabled");
}
ConsoleProxyGCThread cthread = new ConsoleProxyGCThread(connectionMap, removedSessionsSet);
cthread.setName("Console Proxy GC Thread");
cthread.start();
}
private static void startupHttpMain() {
try {
ConsoleProxyServerFactory factory = getHttpServerFactory();
if (factory == null) {
s_logger.error("Unable to load HTTP server factory");
System.exit(1);
}
HttpServer server = factory.createHttpServerInstance(httpListenPort);
server.createContext("/getscreen", new ConsoleProxyThumbnailHandler());
server.createContext("/resource/", new ConsoleProxyResourceHandler());
server.createContext("/ajax", new ConsoleProxyAjaxHandler());
server.createContext("/ajaximg", new ConsoleProxyAjaxImageHandler());
server.setExecutor(new ThreadExecutor()); // creates a default executor
server.start();
ConsoleProxyNoVNCServer noVNCServer = getNoVNCServer();
noVNCServer.start();
} catch (Exception e) {
s_logger.error(e.getMessage(), e);
System.exit(1);
}
}
private static ConsoleProxyNoVNCServer getNoVNCServer() {
int vncPort = ConsoleProxyNoVNCServer.getVNCPort();
return vncPort == ConsoleProxyNoVNCServer.WSS_PORT ?
new ConsoleProxyNoVNCServer(ksBits, ksPassword) :
new ConsoleProxyNoVNCServer();
}
private static void startupHttpCmdPort() {
try {
s_logger.info("Listening for HTTP CMDs on port " + httpCmdListenPort);
HttpServer cmdServer = HttpServer.create(new InetSocketAddress(httpCmdListenPort), 2);
cmdServer.createContext("/cmd", new ConsoleProxyCmdHandler());
cmdServer.setExecutor(new ThreadExecutor()); // creates a default executor
cmdServer.start();
} catch (Exception e) {
s_logger.error(e.getMessage(), e);
System.exit(1);
}
}
public static void main(String[] argv) {
standaloneStart = true;
configLog4j();
Logger.setFactory(new ConsoleProxyLoggerFactory());
InputStream confs = ConsoleProxy.class.getResourceAsStream("/conf/consoleproxy.properties");
Properties conf = new Properties();
if (confs == null) {
s_logger.info("Can't load consoleproxy.properties from classpath, will use default configuration");
} else {
try {
conf.load(confs);
} catch (Exception e) {
s_logger.error(e.toString(), e);
} finally {
try {
confs.close();
} catch (IOException ioex) {
s_logger.error(ioex.toString(), ioex);
}
}
}
start(conf);
}
public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) throws Exception {
ConsoleProxyClient viewer = null;
boolean reportLoadChange = false;
String clientKey = param.getClientMapKey();
synchronized (connectionMap) {
viewer = connectionMap.get(clientKey);
if (viewer == null || viewer.getClass() == ConsoleProxyNoVncClient.class) {
viewer = getClient(param);
viewer.initClient(param);
connectionMap.put(clientKey, viewer);
s_logger.info("Added viewer object " + viewer);
reportLoadChange = true;
} else if (!viewer.isFrontEndAlive()) {
s_logger.info("The rfb thread died, reinitializing the viewer " + viewer);
viewer.initClient(param);
} else if (!param.getClientHostPassword().equals(viewer.getClientHostPassword())) {
s_logger.warn("Bad sid detected(VNC port may be reused). sid in session: " + viewer.getClientHostPassword() + ", sid in request: " +
param.getClientHostPassword());
viewer.initClient(param);
}
}
if (reportLoadChange) {
ConsoleProxyClientStatsCollector statsCollector = getStatsCollector();
String loadInfo = statsCollector.getStatsReport();
reportLoadInfo(loadInfo);
if (s_logger.isDebugEnabled())
s_logger.debug("Report load change : " + loadInfo);
}
return viewer;
}
public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, String ajaxSession) throws Exception {
boolean reportLoadChange = false;
String clientKey = param.getClientMapKey();
synchronized (connectionMap) {
ConsoleProxyClient viewer = connectionMap.get(clientKey);
if (viewer == null || viewer.getClass() == ConsoleProxyNoVncClient.class) {
authenticationExternally(param);
viewer = getClient(param);
viewer.initClient(param);
connectionMap.put(clientKey, viewer);
s_logger.info("Added viewer object " + viewer);
reportLoadChange = true;
} else {
// protected against malicious attack by modifying URL content
if (ajaxSession != null) {
long ajaxSessionIdFromUrl = Long.parseLong(ajaxSession);
if (ajaxSessionIdFromUrl != viewer.getAjaxSessionId())
throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": modified AJAX session id");
}
if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() ||
!param.getClientHostPassword().equals(viewer.getClientHostPassword()))
throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid");
if (!viewer.isFrontEndAlive()) {
authenticationExternally(param);
viewer.initClient(param);
reportLoadChange = true;
}
}
if (reportLoadChange) {
ConsoleProxyClientStatsCollector statsCollector = getStatsCollector();
String loadInfo = statsCollector.getStatsReport();
reportLoadInfo(loadInfo);
if (s_logger.isDebugEnabled())
s_logger.debug("Report load change : " + loadInfo);
}
return viewer;
}
}
private static ConsoleProxyClient getClient(ConsoleProxyClientParam param) {
if (param.getHypervHost() != null) {
return new ConsoleProxyRdpClient();
} else {
return new ConsoleProxyVncClient();
}
}
public static void removeViewer(ConsoleProxyClient viewer) {
synchronized (connectionMap) {
for (Map.Entry<String, ConsoleProxyClient> entry : connectionMap.entrySet()) {
if (entry.getValue() == viewer) {
connectionMap.remove(entry.getKey());
removedSessionsSet.add(viewer.getSessionUuid());
return;
}
}
}
}
public static ConsoleProxyClientStatsCollector getStatsCollector() {
synchronized (connectionMap) {
return new ConsoleProxyClientStatsCollector(connectionMap);
}
}
public static void authenticationExternally(ConsoleProxyClientParam param) throws AuthenticationException {
ConsoleProxyAuthenticationResult authResult = authenticateConsoleAccess(param, false);
if (authResult == null || !authResult.isSuccess()) {
s_logger.warn("External authenticator failed authentication request for vm " + param.getClientTag() + " with sid " + param.getClientHostPassword());
throw new AuthenticationException("External authenticator failed request for vm " + param.getClientTag() + " with sid " + param.getClientHostPassword());
}
}
public static ConsoleProxyAuthenticationResult reAuthenticationExternally(ConsoleProxyClientParam param) {
return authenticateConsoleAccess(param, true);
}
public static String getEncryptorPassword() {
return encryptorPassword;
}
public static void setEncryptorPassword(String password) {
encryptorPassword = password;
}
public static void setIsSourceIpCheckEnabled(Boolean isEnabled) {
isSourceIpCheckEnabled = isEnabled;
}
static class ThreadExecutor implements Executor {
@Override
public void execute(Runnable r) {
new Thread(r).start();
}
}
public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam param, String ajaxSession,
Session session) throws AuthenticationException {
boolean reportLoadChange = false;
String clientKey = param.getClientMapKey();
synchronized (connectionMap) {
ConsoleProxyClient viewer = connectionMap.get(clientKey);
if (viewer == null || viewer.getClass() != ConsoleProxyNoVncClient.class) {
authenticationExternally(param);
viewer = new ConsoleProxyNoVncClient(session);
viewer.initClient(param);
connectionMap.put(clientKey, viewer);
reportLoadChange = true;
} else {
if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() ||
!param.getClientHostPassword().equals(viewer.getClientHostPassword()))
throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid");
try {
authenticationExternally(param);
} catch (Exception e) {
s_logger.error("Authentication failed for param: " + param);
return null;
}
s_logger.info("Initializing new novnc client and disconnecting existing session");
try {
((ConsoleProxyNoVncClient)viewer).getSession().disconnect();
} catch (IOException e) {
s_logger.error("Exception while disconnect session of novnc viewer object: " + viewer, e);
}
removeViewer(viewer);
viewer = new ConsoleProxyNoVncClient(session);
viewer.initClient(param);
connectionMap.put(clientKey, viewer);
reportLoadChange = true;
}
if (reportLoadChange) {
ConsoleProxyClientStatsCollector statsCollector = getStatsCollector();
String loadInfo = statsCollector.getStatsReport();
reportLoadInfo(loadInfo);
if (s_logger.isDebugEnabled())
s_logger.debug("Report load change : " + loadInfo);
}
return (ConsoleProxyNoVncClient)viewer;
}
}
}