Support console access through rebooting with XAPI session re-negotiation

This commit is contained in:
Kelven Yang 2012-05-16 17:02:22 -07:00
parent 297996e907
commit e8a5d51da7
13 changed files with 377 additions and 101 deletions

View File

@ -0,0 +1,64 @@
package com.cloud.agent.resource.consoleproxy;
public class ConsoleProxyAuthenticationResult {
private boolean success;
private boolean isReauthentication;
private String host;
private int port;
private String tunnelUrl;
private String tunnelSession;
public ConsoleProxyAuthenticationResult() {
success = false;
isReauthentication = false;
port = 0;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public boolean isReauthentication() {
return isReauthentication;
}
public void setReauthentication(boolean isReauthentication) {
this.isReauthentication = isReauthentication;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getTunnelUrl() {
return tunnelUrl;
}
public void setTunnelUrl(String tunnelUrl) {
this.tunnelUrl = tunnelUrl;
}
public String getTunnelSession() {
return tunnelSession;
}
public void setTunnelSession(String tunnelSession) {
this.tunnelSession = tunnelSession;
}
}

View File

@ -55,6 +55,7 @@ import com.cloud.resource.ServerResourceBase;
import com.cloud.utils.NumbersUtil;
import com.cloud.utils.net.NetUtils;
import com.cloud.utils.script.Script;
import com.google.gson.Gson;
/**
*
@ -417,28 +418,36 @@ public class ConsoleProxyResource extends ServerResourceBase implements
}
}
public boolean authenticateConsoleAccess(String host, String port,
String vmId, String sid, String ticket) {
public String authenticateConsoleAccess(String host, String port,
String vmId, String sid, String ticket, Boolean isReauthentication) {
ConsoleAccessAuthenticationCommand cmd = new ConsoleAccessAuthenticationCommand(
host, port, vmId, sid, ticket);
cmd.setReauthenticating(isReauthentication);
ConsoleProxyAuthenticationResult result = new ConsoleProxyAuthenticationResult();
result.setSuccess(false);
result.setReauthentication(isReauthentication);
try {
AgentControlAnswer answer = getAgentControl().sendRequest(cmd,
10000);
AgentControlAnswer answer = getAgentControl().sendRequest(cmd, 10000);
if (answer != null) {
return ((ConsoleAccessAuthenticationAnswer) answer).succeeded();
ConsoleAccessAuthenticationAnswer authAnswer = (ConsoleAccessAuthenticationAnswer)answer;
result.setSuccess(authAnswer.succeeded());
result.setHost(authAnswer.getHost());
result.setPort(authAnswer.getPort());
result.setTunnelUrl(authAnswer.getTunnelUrl());
result.setTunnelSession(authAnswer.getTunnelSession());
} else {
s_logger.error("Authentication failed for vm: " + vmId
+ " with sid: " + sid);
s_logger.error("Authentication failed for vm: " + vmId + " with sid: " + sid);
}
} catch (AgentControlChannelException e) {
s_logger.error(
"Unable to send out console access authentication request due to "
+ e.getMessage(), e);
s_logger.error("Unable to send out console access authentication request due to "
+ e.getMessage(), e);
}
return false;
return new Gson().toJson(result);
}
public void reportLoadInfo(String gsonLoadInfo) {

View File

@ -10,21 +10,75 @@
// limitations under the License.
//
// Automatically generated by addcopyright.py at 04/03/2012
package com.cloud.agent.api;
public class ConsoleAccessAuthenticationAnswer extends AgentControlAnswer {
package com.cloud.agent.api;
public class ConsoleAccessAuthenticationAnswer extends AgentControlAnswer {
private boolean _success;
private boolean _isReauthenticating;
private String _host;
private int _port;
private String _tunnelUrl;
private String _tunnelSession;
public ConsoleAccessAuthenticationAnswer() {
_success = false;
_isReauthenticating = false;
_port = 0;
}
public ConsoleAccessAuthenticationAnswer(Command cmd, boolean success) {
super(cmd);
_success = success;
}
public boolean succeeded() {
return _success;
}
public ConsoleAccessAuthenticationAnswer(Command cmd, boolean success) {
super(cmd);
_success = success;
public void setSuccess(boolean value) {
_success = value;
}
public boolean succeeded() {
return _success;
public boolean isReauthenticating() {
return _isReauthenticating;
}
}
public void setReauthenticating(boolean value) {
_isReauthenticating = value;
}
public String getHost() {
return _host;
}
public void setHost(String host) {
_host = host;
}
public int getPort() {
return _port;
}
public void setPort(int port) {
_port = port;
}
public String getTunnelUrl() {
return _tunnelUrl;
}
public void setTunnelUrl(String tunnelUrl) {
_tunnelUrl = tunnelUrl;
}
public String getTunnelSession() {
return _tunnelSession;
}
public void setTunnelSession(String tunnelSession) {
_tunnelSession = tunnelSession;
}
}

View File

@ -20,9 +20,12 @@ public class ConsoleAccessAuthenticationCommand extends AgentControlCommand {
private String _sid;
private String _ticket;
private boolean _isReauthenticating;
public ConsoleAccessAuthenticationCommand() {
}
_isReauthenticating = false;
}
public ConsoleAccessAuthenticationCommand(String host, String port, String vmId, String sid, String ticket) {
_host = host;
_port = port;
@ -50,4 +53,12 @@ public class ConsoleAccessAuthenticationCommand extends AgentControlCommand {
public String getTicket() {
return _ticket;
}
}
public boolean isReauthenticating() {
return _isReauthenticating;
}
public void setReauthenticating(boolean value) {
_isReauthenticating = value;
}
}

View File

@ -30,6 +30,7 @@ import org.apache.axis.encoding.Base64;
import org.apache.log4j.xml.DOMConfigurator;
import com.cloud.consoleproxy.util.Logger;
import com.google.gson.Gson;
import com.sun.net.httpserver.HttpServer;
/**
@ -171,9 +172,17 @@ public class ConsoleProxy {
}
}
public static boolean authenticateConsoleAccess(ConsoleProxyClientParam param) {
if(standaloneStart)
return true;
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(standaloneStart) {
return authResult;
}
if(authMethod != null) {
Object result;
@ -183,26 +192,29 @@ public class ConsoleProxy {
String.valueOf(param.getClientHostPort()),
param.getClientTag(),
param.getClientHostPassword(),
param.getTicket());
param.getTicket(),
new Boolean(reauthentication));
} catch (IllegalAccessException e) {
s_logger.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException" + " for vm: " + param.getClientTag(), e);
return false;
authResult.setSuccess(false);
return authResult;
} catch (InvocationTargetException e) {
s_logger.error("Unable to invoke authenticateConsoleAccess due to InvocationTargetException " + " for vm: " + param.getClientTag(), e);
return false;
authResult.setSuccess(false);
return authResult;
}
if(result != null && result instanceof Boolean) {
return ((Boolean)result).booleanValue();
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");
return false;
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 true;
}
}
return authResult;
}
public static void reportLoadInfo(String gsonLoadInfo) {
@ -250,7 +262,7 @@ public class ConsoleProxy {
ConsoleProxy.ksPassword = ksPassword;
try {
Class<?> contextClazz = Class.forName("com.cloud.agent.resource.consoleproxy.ConsoleProxyResource");
authMethod = contextClazz.getDeclaredMethod("authenticateConsoleAccess", String.class, String.class, String.class, String.class, String.class);
authMethod = contextClazz.getDeclaredMethod("authenticateConsoleAccess", String.class, String.class, String.class, String.class, String.class, Boolean.class);
reportMethod = contextClazz.getDeclaredMethod("reportLoadInfo", String.class);
ensureRouteMethod = contextClazz.getDeclaredMethod("ensureRoute", String.class);
} catch (SecurityException e) {
@ -454,14 +466,20 @@ public class ConsoleProxy {
return new ConsoleProxyClientStatsCollector(connectionMap);
}
public static void authenticationExternally(ConsoleProxyClientParam param) throws AuthenticationException {
if(!authenticateConsoleAccess(param)) {
public static void authenticationExternally(ConsoleProxyClientParam param) throws AuthenticationException {
ConsoleProxyAuthenticationResult authResult = authenticateConsoleAccess(param, false);
if(authResult == null || !authResult.isSuccess()) {
s_logger.warn("External authenticator failed authencation 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;
}

View File

@ -82,7 +82,7 @@ public class ConsoleProxyAjaxHandler implements HttpHandler {
int port;
if(host == null || portStr == null || sid == null)
if(host == null || portStr == null || sid == null)
throw new IllegalArgumentException();
try {
@ -159,8 +159,7 @@ public class ConsoleProxyAjaxHandler implements HttpHandler {
}
} else {
if(ajaxSessionId != 0 && ajaxSessionId != viewer.getAjaxSessionId()) {
if(s_logger.isDebugEnabled())
s_logger.debug("Ajax request comes from a different session, id in request: " + ajaxSessionId + ", id in viewer: " + viewer.getAjaxSessionId());
s_logger.info("Ajax request comes from a different session, id in request: " + ajaxSessionId + ", id in viewer: " + viewer.getAjaxSessionId());
handleClientKickoff(t, viewer);
} else if(ajaxSessionId == 0) {
if(s_logger.isDebugEnabled())

View File

@ -0,0 +1,65 @@
package com.cloud.consoleproxy;
// duplicated class
public class ConsoleProxyAuthenticationResult {
private boolean success;
private boolean isReauthentication;
private String host;
private int port;
private String tunnelUrl;
private String tunnelSession;
public ConsoleProxyAuthenticationResult() {
success = false;
isReauthentication = false;
port = 0;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public boolean isReauthentication() {
return isReauthentication;
}
public void setReauthentication(boolean isReauthentication) {
this.isReauthentication = isReauthentication;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getTunnelUrl() {
return tunnelUrl;
}
public void setTunnelUrl(String tunnelUrl) {
this.tunnelUrl = tunnelUrl;
}
public String getTunnelSession() {
return tunnelSession;
}
public void setTunnelSession(String tunnelSession) {
this.tunnelSession = tunnelSession;
}
}

View File

@ -48,8 +48,10 @@ public class ConsoleProxyVncClient extends ConsoleProxyClientBase {
@Override
public boolean isFrontEndAlive() {
if(workerDone || System.currentTimeMillis() - getClientLastFrontEndActivityTime() > ConsoleProxy.VIEWER_LINGER_SECONDS*1000)
return false;
if(workerDone || System.currentTimeMillis() - getClientLastFrontEndActivityTime() > ConsoleProxy.VIEWER_LINGER_SECONDS*1000) {
s_logger.info("Front end has been idle for too long");
return false;
}
return true;
}
@ -57,34 +59,28 @@ public class ConsoleProxyVncClient extends ConsoleProxyClientBase {
public void initClient(ConsoleProxyClientParam param) {
setClientParam(param);
final String tunnelUrl = param.getClientTunnelUrl();
final String tunnelSession = param.getClientTunnelSession();
client = new VncClient(this);
worker = new Thread(new Runnable() {
public void run() {
long startTick = System.currentTimeMillis();
while(System.currentTimeMillis() - startTick < 7000) {
String tunnelUrl = getClientParam().getClientTunnelUrl();
String tunnelSession = getClientParam().getClientTunnelSession();
for(int i = 0; i < 15; i++) {
try {
if(tunnelUrl != null && !tunnelUrl.isEmpty() && tunnelSession != null && !tunnelSession.isEmpty()) {
try {
URI uri = new URI(tunnelUrl);
s_logger.info("Connect to VNC server via tunnel. url: " + tunnelUrl + ", session: " + tunnelSession);
client.connectTo(
uri.getHost(), uri.getPort(),
uri.getPath() + "?" + uri.getQuery(),
tunnelSession, "https".equalsIgnoreCase(uri.getScheme()),
getClientHostPassword());
} catch (URISyntaxException e) {
s_logger.warn("Invalid tunnel URL " + tunnelUrl);
}
URI uri = new URI(tunnelUrl);
s_logger.info("Connect to VNC server via tunnel. url: " + tunnelUrl + ", session: " + tunnelSession);
client.connectTo(
uri.getHost(), uri.getPort(),
uri.getPath() + "?" + uri.getQuery(),
tunnelSession, "https".equalsIgnoreCase(uri.getScheme()),
getClientHostPassword());
} else {
s_logger.info("Connect to VNC server directly. host: " + getClientHostAddress() + ", port: " + getClientHostPort());
client.connectTo(getClientHostAddress(), getClientHostPort(), getClientHostPassword());
}
} catch (UnknownHostException e) {
s_logger.error("Unexpected exception", e);
break;
s_logger.error("Unexpected exception (will retry until timeout)", e);
} catch (IOException e) {
s_logger.error("Unexpected exception (will retry until timeout) ", e);
} catch (Throwable e) {
@ -94,11 +90,26 @@ public class ConsoleProxyVncClient extends ConsoleProxyClientBase {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
if(tunnelUrl != null && !tunnelUrl.isEmpty() && tunnelSession != null && !tunnelSession.isEmpty()) {
ConsoleProxyAuthenticationResult authResult = ConsoleProxy.reAuthenticationExternally(getClientParam());
if(authResult != null && authResult.isSuccess()) {
if(authResult.getTunnelUrl() != null && !authResult.getTunnelUrl().isEmpty() &&
authResult.getTunnelSession() != null && !authResult.getTunnelSession().isEmpty()) {
tunnelUrl = authResult.getTunnelUrl();
tunnelSession = authResult.getTunnelSession();
s_logger.info("Reset XAPI session. url: " + tunnelUrl + ", session: " + tunnelSession);
}
}
}
}
workerDone = true;
}
s_logger.info("Receiver thread stopped.");
workerDone = true;
client.getClientListener().onClientClose();
}
});
worker.setDaemon(true);
@ -115,7 +126,9 @@ public class ConsoleProxyVncClient extends ConsoleProxyClientBase {
public void onClientConnected() {
}
public void onClientClose() {
public void onClientClose() {
s_logger.info("Received client close indication. remove viewer from map.");
ConsoleProxy.removeViewer(this);
}

View File

@ -121,10 +121,12 @@ public class VncClient {
} catch (Throwable e) {
}
}
clientListener.onClientClose();
}
public ConsoleProxyClientListener getClientListener() {
return clientListener;
}
public void connectTo(String host, int port, String path,
String session, boolean useSSL, String sid) throws UnknownHostException, IOException {
if(port < 0) {

View File

@ -22,6 +22,7 @@ import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import com.cloud.consoleproxy.util.Logger;
import com.cloud.consoleproxy.vnc.packet.client.ClientPacket;
import com.cloud.consoleproxy.vnc.packet.client.FramebufferUpdateRequestPacket;
import com.cloud.consoleproxy.vnc.packet.client.KeyboardEventPacket;
@ -30,6 +31,7 @@ import com.cloud.consoleproxy.vnc.packet.client.SetEncodingsPacket;
import com.cloud.consoleproxy.vnc.packet.client.SetPixelFormatPacket;
public class VncClientPacketSender implements Runnable, PaintNotificationListener, KeyListener, MouseListener, MouseMotionListener, FrameBufferUpdateListener {
private static final Logger s_logger = Logger.getLogger(VncClientPacketSender.class);
// Queue for outgoing packets
private final BlockingQueue<ClientPacket> queue = new ArrayBlockingQueue<ClientPacket>(30);
@ -68,6 +70,7 @@ public class VncClientPacketSender implements Runnable, PaintNotificationListene
}
}
} catch (Throwable e) {
s_logger.error("Unexpected exception: ", e);
if (connectionAlive) {
closeConnection();
vncConnection.shutdown();

View File

@ -83,7 +83,7 @@ public class VncServerPacketReceiver implements Runnable {
}
}
} catch (Throwable e) {
s_logger.error("Unexpected exception: ", e);
if (connectionAlive) {
closeConnection();
vncConnection.shutdown();

View File

@ -33,6 +33,8 @@ import com.cloud.agent.api.Answer;
import com.cloud.agent.api.ConsoleAccessAuthenticationAnswer;
import com.cloud.agent.api.ConsoleAccessAuthenticationCommand;
import com.cloud.agent.api.ConsoleProxyLoadReportCommand;
import com.cloud.agent.api.GetVncPortAnswer;
import com.cloud.agent.api.GetVncPortCommand;
import com.cloud.agent.api.RebootCommand;
import com.cloud.agent.api.StartupCommand;
import com.cloud.agent.api.StartupProxyCommand;
@ -109,6 +111,7 @@ import com.cloud.user.User;
import com.cloud.utils.DateUtil;
import com.cloud.utils.NumbersUtil;
import com.cloud.utils.Pair;
import com.cloud.utils.Ternary;
import com.cloud.utils.component.Adapters;
import com.cloud.utils.component.ComponentLocator;
import com.cloud.utils.component.Inject;
@ -865,25 +868,27 @@ public class ConsoleProxyManagerImpl implements ConsoleProxyManager, ConsoleProx
s_logger.debug("Console authentication. Ticket in url for " + cmd.getHost() + ":" + cmd.getPort() + "-" + cmd.getVmId() + " is " + ticketInUrl);
}
String ticket = ConsoleProxyServlet.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId());
if (s_logger.isDebugEnabled()) {
s_logger.debug("Console authentication. Ticket in 1 minute boundary for " + cmd.getHost() + ":" + cmd.getPort() + "-" + cmd.getVmId() + " is " + ticket);
}
if (!ticket.equals(ticketInUrl)) {
Date now = new Date();
// considering of minute round-up
String minuteEarlyTicket = ConsoleProxyServlet.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId(), new Date(now.getTime() - 60 * 1000));
if (s_logger.isDebugEnabled()) {
s_logger.debug("Console authentication. Ticket in 2-minute boundary for " + cmd.getHost() + ":" + cmd.getPort() + "-" + cmd.getVmId() + " is " + minuteEarlyTicket);
}
if (!minuteEarlyTicket.equals(ticketInUrl)) {
s_logger.error("Access ticket expired or has been modified. vmId: " + cmd.getVmId() + "ticket in URL: " + ticketInUrl + ", tickets to check against: " + ticket + ","
+ minuteEarlyTicket);
return new ConsoleAccessAuthenticationAnswer(cmd, false);
}
if(!cmd.isReauthenticating()) {
String ticket = ConsoleProxyServlet.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId());
if (s_logger.isDebugEnabled()) {
s_logger.debug("Console authentication. Ticket in 1 minute boundary for " + cmd.getHost() + ":" + cmd.getPort() + "-" + cmd.getVmId() + " is " + ticket);
}
if (!ticket.equals(ticketInUrl)) {
Date now = new Date();
// considering of minute round-up
String minuteEarlyTicket = ConsoleProxyServlet.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId(), new Date(now.getTime() - 60 * 1000));
if (s_logger.isDebugEnabled()) {
s_logger.debug("Console authentication. Ticket in 2-minute boundary for " + cmd.getHost() + ":" + cmd.getPort() + "-" + cmd.getVmId() + " is " + minuteEarlyTicket);
}
if (!minuteEarlyTicket.equals(ticketInUrl)) {
s_logger.error("Access ticket expired or has been modified. vmId: " + cmd.getVmId() + "ticket in URL: " + ticketInUrl + ", tickets to check against: " + ticket + ","
+ minuteEarlyTicket);
return new ConsoleAccessAuthenticationAnswer(cmd, false);
}
}
}
if (cmd.getVmId() != null && cmd.getVmId().isEmpty()) {
@ -899,10 +904,6 @@ public class ConsoleProxyManagerImpl implements ConsoleProxyManager, ConsoleProx
return new ConsoleAccessAuthenticationAnswer(cmd, false);
}
// TODO authentication channel between console proxy VM and management
// server needs to be secured,
// the data is now being sent through private network, but this is
// apparently not enough
VMInstanceVO vm = _instanceDao.findById(vmId);
if (vm == null) {
return new ConsoleAccessAuthenticationAnswer(cmd, false);
@ -924,6 +925,40 @@ public class ConsoleProxyManagerImpl implements ConsoleProxyManager, ConsoleProx
s_logger.warn("sid " + sid + " in url does not match stored sid " + vm.getVncPassword());
return new ConsoleAccessAuthenticationAnswer(cmd, false);
}
if(cmd.isReauthenticating()) {
ConsoleAccessAuthenticationAnswer authenticationAnswer = new ConsoleAccessAuthenticationAnswer(cmd, true);
authenticationAnswer.setReauthenticating(true);
s_logger.info("Re-authentication request, ask host " + vm.getHostId() + " for new console info");
GetVncPortAnswer answer = (GetVncPortAnswer) _agentMgr.easySend(vm.getHostId(), new
GetVncPortCommand(vm.getId(), vm.getInstanceName()));
if (answer != null && answer.getResult()) {
Ternary<String, String, String> parsedHostInfo = ConsoleProxyServlet.parseHostInfo(answer.getAddress());
if(parsedHostInfo.second() != null && parsedHostInfo.third() != null) {
s_logger.info("Re-authentication result. vm: " + vm.getId() + ", tunnel url: " + parsedHostInfo.second()
+ ", tunnel session: " + parsedHostInfo.third());
authenticationAnswer.setTunnelUrl(parsedHostInfo.second());
authenticationAnswer.setTunnelSession(parsedHostInfo.third());
} else {
s_logger.info("Re-authentication result. vm: " + vm.getId() + ", host address: " + parsedHostInfo.first()
+ ", port: " + answer.getPort());
authenticationAnswer.setHost(parsedHostInfo.first());
authenticationAnswer.setPort(answer.getPort());
}
} else {
s_logger.warn("Re-authentication request failed");
authenticationAnswer.setSuccess(false);
}
return authenticationAnswer;
}
return new ConsoleAccessAuthenticationAnswer(cmd, true);
}

View File

@ -13,8 +13,6 @@
package com.cloud.servlet;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
@ -40,7 +38,6 @@ import com.cloud.server.ManagementServer;
import com.cloud.storage.GuestOSVO;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.DomainManager;
import com.cloud.user.User;
import com.cloud.uservm.UserVm;
import com.cloud.utils.Pair;
@ -286,17 +283,23 @@ public class ConsoleProxyServlet extends HttpServlet {
}
// put the ugly stuff here
static private Ternary<String, String, String> parseHostInfo(String hostInfo) {
static public Ternary<String, String, String> parseHostInfo(String hostInfo) {
String host = null;
String tunnelUrl = null;
String tunnelSession = null;
s_logger.info("Parse host info returned from executing GetVNCPortCommand. host info: " + hostInfo);
if(hostInfo != null && hostInfo.startsWith("consoleurl")) {
String tokens[] = hostInfo.split("&");
host = hostInfo.substring(19, hostInfo.indexOf('/', 19)).trim();
tunnelUrl = tokens[0].substring("consoleurl=".length());
tunnelSession = tokens[1].split("=")[1];
if(hostInfo.length() > 19 && hostInfo.indexOf('/', 19) > 19) {
host = hostInfo.substring(19, hostInfo.indexOf('/', 19)).trim();
tunnelUrl = tokens[0].substring("consoleurl=".length());
tunnelSession = tokens[1].split("=")[1];
} else {
host = "";
}
} else {
host = hostInfo;
}