From d76207a09cb3c220eed7bd2acca1b47cea82885e Mon Sep 17 00:00:00 2001 From: Kelven Yang Date: Mon, 9 Apr 2012 15:38:58 -0700 Subject: [PATCH] Add support for Xen Secure-Console Intergration in console proxy --- .../com/cloud/consoleproxy/ConsoleProxy.java | 67 ++--- .../consoleproxy/ConsoleProxyAjaxHandler.java | 11 +- .../ConsoleProxyAjaxImageHandler.java | 10 +- .../consoleproxy/ConsoleProxyClient.java | 3 +- .../consoleproxy/ConsoleProxyClientBase.java | 3 +- .../consoleproxy/ConsoleProxyClientParam.java | 75 ++++++ .../ConsoleProxyThumbnailHandler.java | 10 +- .../consoleproxy/ConsoleProxyVncClient.java | 15 +- .../com/cloud/consoleproxy/util/RawHTTP.java | 245 ++++++++++++++++++ .../com/cloud/consoleproxy/vnc/VncClient.java | 12 + 10 files changed, 402 insertions(+), 49 deletions(-) create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientParam.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/util/RawHTTP.java diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java index bab8ca50261..bad33a2b072 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java @@ -151,31 +151,36 @@ public class ConsoleProxy { } } - public static boolean authenticateConsoleAccess(String host, String port, String vmId, String sid, String ticket) { + public static boolean authenticateConsoleAccess(ConsoleProxyClientParam param) { if(standaloneStart) return true; if(authMethod != null) { Object result; try { - result = authMethod.invoke(ConsoleProxy.context, host, port, vmId, sid, ticket); + result = authMethod.invoke(ConsoleProxy.context, + param.getClientHostAddress(), + String.valueOf(param.getClientHostPort()), + param.getClientTag(), + param.getClientHostPassword(), + param.getTicket()); } catch (IllegalAccessException e) { - s_logger.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException" + " for vm: " + vmId, e); + s_logger.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException" + " for vm: " + param.getClientTag(), e); return false; } catch (InvocationTargetException e) { - s_logger.error("Unable to invoke authenticateConsoleAccess due to InvocationTargetException " + " for vm: " + vmId, e); + s_logger.error("Unable to invoke authenticateConsoleAccess due to InvocationTargetException " + " for vm: " + param.getClientTag(), e); return false; } if(result != null && result instanceof Boolean) { return ((Boolean)result).booleanValue(); } else { - s_logger.error("Invalid authentication return object " + result + " for vm: " + vmId + ", decline the access"); + s_logger.error("Invalid authentication return object " + result + " for vm: " + param.getClientTag() + ", decline the access"); return false; } } else { - s_logger.warn("Private channel towards management server is not setup. Switch to offline mode and allow access to vm: " + vmId); + s_logger.warn("Private channel towards management server is not setup. Switch to offline mode and allow access to vm: " + param.getClientTag()); return true; } } @@ -343,29 +348,29 @@ public class ConsoleProxy { start(conf); } - public static ConsoleProxyClient getVncViewer(String host, int port, String sid, String tag, String ticket) throws Exception { + public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) throws Exception { ConsoleProxyClient viewer = null; - boolean reportLoadChange = false; + boolean reportLoadChange = false; + String clientKey = param.getClientMapKey(); synchronized (connectionMap) { - viewer = connectionMap.get(host + ":" + port); + viewer = connectionMap.get(clientKey); if (viewer == null) { viewer = new ConsoleProxyVncClient(); - viewer.initClient(host, port, sid, tag, ticket); - connectionMap.put(host + ":" + port, viewer); + 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(host, port, sid, tag, ticket); + s_logger.info("The rfb thread died, reinitializing the viewer " + viewer); + viewer.initClient(param); reportLoadChange = true; - } else if (!sid.equals(viewer.getClientHostPassword())) { + } 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: " + sid); - viewer.initClient(host, port, sid, tag, ticket); + + ", sid in request: " + param.getClientHostPassword()); + viewer.initClient(param); reportLoadChange = true; } @@ -382,32 +387,32 @@ public class ConsoleProxy { return viewer; } - public static ConsoleProxyClient getAjaxVncViewer(String host, int port, String sid, String tag, - String ticket, String ajaxSession) throws Exception { + public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, String ajaxSession) throws Exception { boolean reportLoadChange = false; + String clientKey = param.getClientMapKey(); synchronized (connectionMap) { - ConsoleProxyClient viewer = connectionMap.get(host + ":" + port); + ConsoleProxyClient viewer = connectionMap.get(clientKey); if (viewer == null) { viewer = new ConsoleProxyVncClient(); - viewer.initClient(host, port, sid, tag, ticket); + viewer.initClient(param); - connectionMap.put(host + ":" + port, viewer); + 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(host, port, sid, tag, ticket); + viewer.initClient(param); reportLoadChange = true; - } else if (!sid.equals(viewer.getClientHostPassword())) { + } 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: " + sid); - viewer.initClient(host, port, sid, tag, ticket); + + viewer.getClientHostPassword() + ", sid in request: " + param.getClientHostPassword()); + viewer.initClient(param); reportLoadChange = true; } else { if(ajaxSession == null || ajaxSession.isEmpty()) - authenticationExternally(host, String.valueOf(port), tag, sid, ticket); + authenticationExternally(param); } if(reportLoadChange) { @@ -436,11 +441,11 @@ public class ConsoleProxy { return new ConsoleProxyClientStatsCollector(connectionMap); } - public static void authenticationExternally(String host, String port, String tag, String sid, String ticket) throws AuthenticationException { - if(!authenticateConsoleAccess(host, port, tag, sid, ticket)) { - s_logger.warn("External authenticator failed authencation request for vm " + tag + " with sid " + sid); + public static void authenticationExternally(ConsoleProxyClientParam param) throws AuthenticationException { + if(!authenticateConsoleAccess(param)) { + 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 " + tag + " with sid " + sid); + throw new AuthenticationException("External authenticator failed request for vm " + param.getClientTag() + " with sid " + param.getClientHostPassword()); } } diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java index 62458189f05..cef028591cf 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java @@ -109,8 +109,15 @@ public class ConsoleProxyAjaxHandler implements HttpHandler { } ConsoleProxyClient viewer = null; - try { - viewer = ConsoleProxy.getAjaxVncViewer(host, port, sid, tag, ticket, ajaxSessionIdStr); + try { + ConsoleProxyClientParam param = new ConsoleProxyClientParam(); + param.setClientHostAddress(host); + param.setClientHostPort(port); + param.setClientHostPassword(sid); + param.setClientTag(tag); + param.setTicket(ticket); + + viewer = ConsoleProxy.getAjaxVncViewer(param, ajaxSessionIdStr); } catch(Exception e) { s_logger.warn("Failed to create viewer due to " + e.getMessage(), e); diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java index 3195927bae8..3b5d726b67d 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java @@ -84,8 +84,14 @@ public class ConsoleProxyAjaxImageHandler implements HttpHandler { s_logger.warn("Invalid numeric parameter in query string: " + keyStr); throw new IllegalArgumentException(e); } - - ConsoleProxyClient viewer = ConsoleProxy.getVncViewer(host, port, sid, tag, ticket); + + ConsoleProxyClientParam param = new ConsoleProxyClientParam(); + param.setClientHostAddress(host); + param.setClientHostPort(port); + param.setClientHostPassword(sid); + param.setClientTag(tag); + param.setTicket(ticket); + ConsoleProxyClient viewer = ConsoleProxy.getVncViewer(param); byte[] img = viewer.getAjaxImageCache().getImage(key); if(img != null) { Headers hds = t.getResponseHeaders(); diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClient.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClient.java index 576bc4a1046..575c4069675 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClient.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClient.java @@ -61,7 +61,6 @@ public interface ConsoleProxyClient { // // Setup/house-keeping // - void initClient(String clientHostAddress, int clientHostPort, - String clientHostPassword, String clientTag, String ticket); + void initClient(ConsoleProxyClientParam param); void closeClient(); } diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientBase.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientBase.java index 091be1a5ed1..38907cd5d1d 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientBase.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientBase.java @@ -124,8 +124,7 @@ public abstract class ConsoleProxyClientBase implements ConsoleProxyClient, Cons } @Override - public abstract void initClient(String clientHostAddress, int clientHostPort, - String clientHostPassword, String clientTag, String ticket); + public abstract void initClient(ConsoleProxyClientParam param); @Override public abstract void closeClient(); diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientParam.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientParam.java new file mode 100644 index 00000000000..b4f9713c4f4 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientParam.java @@ -0,0 +1,75 @@ +// Copyright 2012 Citrix Systems, Inc. Licensed under the +// Apache License, Version 2.0 (the "License"); you may not use this +// file except in compliance with the License. Citrix Systems, Inc. +// reserves all rights not expressly granted by 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. +// +// Automatically generated by addcopyright.py at 04/03/2012 +package com.cloud.consoleproxy; + +/** + * + * @author Kelven Yang + * Data object to store parameter info needed by client to connect to its host + */ +public class ConsoleProxyClientParam { + + private String clientHostAddress; + private int clientHostPort; + private String clientHostPassword; + private String clientTag; + private String ticket; + + public ConsoleProxyClientParam() { + clientHostPort = 0; + } + + public String getClientHostAddress() { + return clientHostAddress; + } + + public void setClientHostAddress(String clientHostAddress) { + this.clientHostAddress = clientHostAddress; + } + + public int getClientHostPort() { + return clientHostPort; + } + + public void setClientHostPort(int clientHostPort) { + this.clientHostPort = clientHostPort; + } + + public String getClientHostPassword() { + return clientHostPassword; + } + + public void setClientHostPassword(String clientHostPassword) { + this.clientHostPassword = clientHostPassword; + } + + public String getClientTag() { + return clientTag; + } + + public void setClientTag(String clientTag) { + this.clientTag = clientTag; + } + + public String getTicket() { + return ticket; + } + + public void setTicket(String ticket) { + this.ticket = ticket; + } + + public String getClientMapKey() { + return clientHostAddress + ":" + clientHostPort; + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyThumbnailHandler.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyThumbnailHandler.java index 87fdbace200..226df7b78ec 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyThumbnailHandler.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyThumbnailHandler.java @@ -120,8 +120,14 @@ public class ConsoleProxyThumbnailHandler implements HttpHandler { } catch (NumberFormatException e) { throw new IllegalArgumentException(e); } - - ConsoleProxyClient viewer = ConsoleProxy.getVncViewer(host, port, sid, tag, ticket); + + ConsoleProxyClientParam param = new ConsoleProxyClientParam(); + param.setClientHostAddress(host); + param.setClientHostPort(port); + param.setClientHostPassword(sid); + param.setClientTag(tag); + param.setTicket(ticket); + ConsoleProxyClient viewer = ConsoleProxy.getVncViewer(param); if (!viewer.isHostConnected()) { // use generated image instead of static diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyVncClient.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyVncClient.java index bfdd675e729..9e383fbe292 100644 --- a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyVncClient.java +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyVncClient.java @@ -52,13 +52,12 @@ public class ConsoleProxyVncClient extends ConsoleProxyClientBase { } @Override - public void initClient(final String clientHostAddress, final int clientHostPort, - final String clientHostPassword, final String clientTag, final String ticket) { - this.host = clientHostAddress; - this.port = clientHostPort; - this.passwordParam = clientHostPassword; - this.tag = clientTag; - this.ticket = ticket; + public void initClient(ConsoleProxyClientParam param) { + this.host = param.getClientHostAddress(); + this.port = param.getClientHostPort(); + this.passwordParam = param.getClientHostPassword(); + this.tag = param.getClientTag(); + this.ticket = param.getTicket(); client = new VncClient(this); worker = new Thread(new Runnable() { @@ -66,7 +65,7 @@ public class ConsoleProxyVncClient extends ConsoleProxyClientBase { long startTick = System.currentTimeMillis(); while(System.currentTimeMillis() - startTick < 7000) { try { - client.connectTo(clientHostAddress, clientHostPort, clientHostPassword); + client.connectTo(host, port, passwordParam); } catch (UnknownHostException e) { s_logger.error("Unexpected exception: ", e); } catch (IOException e) { diff --git a/console-proxy/src/com/cloud/consoleproxy/util/RawHTTP.java b/console-proxy/src/com/cloud/consoleproxy/util/RawHTTP.java new file mode 100644 index 00000000000..f4a42531b42 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/util/RawHTTP.java @@ -0,0 +1,245 @@ +// Copyright 2012 Citrix Systems, Inc. Licensed under the +// Apache License, Version 2.0 (the "License"); you may not use this +// file except in compliance with the License. Citrix Systems, Inc. +// reserves all rights not expressly granted by 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. +// +// Automatically generated by addcopyright.py at 04/03/2012 +package com.cloud.consoleproxy.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +// +// This file is originally from XenConsole with modifications +// + +/** + * Send an HTTP CONNECT or PUT request to a XenAPI host with a Session ID, + * return the connected socket and the Task ID. Used for tunnelling VNC + * connections and import/export operations. + */ +public final class RawHTTP { + private static final Logger s_logger = Logger.getLogger(RawHTTP.class); + + private static final Pattern END_PATTERN = Pattern.compile("^\r\n$"); + private static final Pattern HEADER_PATTERN = Pattern + .compile("^([A-Z_a-z0-9-]+):\\s*(.*)\r\n$"); + private static final Pattern HTTP_PATTERN = Pattern + .compile("^HTTP/\\d+\\.\\d+ (\\d*) (.*)\r\n$"); + + /** + * @uml.property name="command" + */ + private final String command; + /** + * @uml.property name="host" + */ + private final String host; + /** + * @uml.property name="port" + */ + private final int port; + /** + * @uml.property name="path" + */ + private final String path; + /** + * @uml.property name="session" + */ + private final String session; + /** + * @uml.property name="useSSL" + */ + private final boolean useSSL; + + /** + * @uml.property name="responseHeaders" + * @uml.associationEnd qualifier="group:java.lang.String java.lang.String" + */ + private final Map responseHeaders = new HashMap(); + + /** + * @uml.property name="ic" + */ + private InputStream ic; + /** + * @uml.property name="oc" + */ + private OutputStream oc; + /** + * @uml.property name="s" + */ + private Socket s; + + public InputStream getInputStream() { + return ic; + } + + public OutputStream getOutputStream() { + return oc; + } + + public Socket getSocket() { + return s; + } + + public RawHTTP(String command, String host, int port, String path, + String session, boolean useSSL) { + this.command = command; + this.host = host; + this.port = port; + this.path = path; + this.session = session; + this.useSSL = useSSL; + } + + private static final TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + } + }; + + private Socket _getSocket() throws IOException { + if (useSSL) { + SSLContext context = getClientSSLContext(); + if(context == null) + throw new IOException("Unable to setup SSL context"); + + SSLSocket ssl = null; + try { + context.init(null, trustAllCerts, new SecureRandom()); + SocketFactory factory = context.getSocketFactory(); + ssl = (SSLSocket) factory.createSocket(host, port); + /* ssl.setSSLParameters(context.getDefaultSSLParameters()); */ + } catch (IOException e) { + s_logger.error("IOException: " + e.getMessage(), e); + throw e; + } catch (KeyManagementException e) { + s_logger.error("KeyManagementException: " + e.getMessage(), e); + } + return ssl; + } else { + return new Socket(host, port); + } + } + + public Socket connect() throws IOException { + String[] headers = makeHeaders(); + s = _getSocket(); + try { + oc = s.getOutputStream(); + for (String header : headers) { + oc.write(header.getBytes()); + oc.write("\r\n".getBytes()); + } + oc.flush(); + ic = s.getInputStream(); + while (true) { + String line = readline(ic); + + Matcher m = END_PATTERN.matcher(line); + if (m.matches()) { + return s; + } + + m = HEADER_PATTERN.matcher(line); + if (m.matches()) { + responseHeaders.put(m.group(1), m.group(2)); + continue; + } + + m = HTTP_PATTERN.matcher(line); + if (m.matches()) { + String status_code = m.group(1); + String reason_phrase = m.group(2); + if (!"200".equals(status_code)) { + throw new IOException("HTTP status " + status_code + + " " + reason_phrase); + } + } else { + throw new IOException("Unknown HTTP line " + line); + } + } + } catch (IOException exn) { + s.close(); + throw exn; + } catch (RuntimeException exn) { + s.close(); + throw exn; + } + } + + public Map getResponseHeaders() { + return responseHeaders; + } + + private String[] makeHeaders() { + String[] headers = { String.format("%s %s HTTP/1.0", command, path), + String.format("Host: %s", host), + String.format("Cookie: session_id=%s", session), "" }; + return headers; + } + + private static String readline(InputStream ic) throws IOException { + String result = ""; + while (true) { + try { + int c = ic.read(); + + if (c == -1) { + return result; + } + result = result + (char) c; + if (c == 0x0a /* LF */) { + return result; + } + } catch (IOException e) { + ic.close(); + throw e; + } + } + } + + private SSLContext getClientSSLContext() { + SSLContext sslContext = null; + try { + sslContext = SSLContext.getInstance("SSL", "SunJSSE"); + } catch (NoSuchAlgorithmException e) { + s_logger.error("Unexpected exception ", e); + } catch (NoSuchProviderException e) { + s_logger.error("Unexpected exception ", e); + } + return sslContext; + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/vnc/VncClient.java b/console-proxy/src/com/cloud/consoleproxy/vnc/VncClient.java index 78eceec36fb..b42df505f95 100644 --- a/console-proxy/src/com/cloud/consoleproxy/vnc/VncClient.java +++ b/console-proxy/src/com/cloud/consoleproxy/vnc/VncClient.java @@ -30,6 +30,7 @@ import javax.crypto.spec.DESKeySpec; import com.cloud.consoleproxy.ConsoleProxyClientListener; import com.cloud.consoleproxy.util.Logger; +import com.cloud.consoleproxy.util.RawHTTP; import com.cloud.consoleproxy.vnc.packet.client.KeyboardEventPacket; import com.cloud.consoleproxy.vnc.packet.client.MouseEventPacket; @@ -124,10 +125,21 @@ public class VncClient { clientListener.onClientClose(); } + public void connectTo(String host, int port, String path, + String session, boolean useSSL) throws UnknownHostException, IOException { + RawHTTP tunnel = new RawHTTP("CONNECT", host, port, path, session, useSSL); + this.socket = tunnel.connect(); + doConnect(""); + } + public void connectTo(String host, int port, String password) throws UnknownHostException, IOException { // Connect to server s_logger.info("Connecting to VNC server " + host + ":" + port + "..."); this.socket = new Socket(host, port); + doConnect(password); + } + + private void doConnect(String password) throws IOException { is = new DataInputStream(socket.getInputStream()); os = new DataOutputStream(socket.getOutputStream());