Skip to content

Commit 7aba8b1

Browse files
committed
Adding noVNC support
1 parent 6d85361 commit 7aba8b1

File tree

12 files changed

+820
-7
lines changed

12 files changed

+820
-7
lines changed

server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import com.cloud.utils.component.Manager;
2020
import com.cloud.vm.ConsoleProxyVO;
2121

22+
import org.apache.cloudstack.framework.config.ConfigKey;
23+
2224
public interface ConsoleProxyManager extends Manager, ConsoleProxyService {
2325

2426
public static final int DEFAULT_PROXY_CAPACITY = 50;
@@ -31,9 +33,14 @@ public interface ConsoleProxyManager extends Manager, ConsoleProxyService {
3133
public static final int DEFAULT_PROXY_URL_PORT = 80;
3234
public static final int DEFAULT_PROXY_SESSION_TIMEOUT = 300000; // 5 minutes
3335

36+
public static final int DEFAULT_NOVNC_PORT = 8080;
37+
3438
public static final String ALERT_SUBJECT = "proxy-alert";
3539
public static final String CERTIFICATE_NAME = "CPVMCertificate";
3640

41+
public static final ConfigKey<Boolean> NoVncConsoleDefault = new ConfigKey<Boolean>("Advanced", Boolean.class, "novnc.console.default", "true",
42+
"If true, noVNC console will be default console for virtual machines", true);
43+
3744
public void setManagementState(ConsoleProxyManagementState state);
3845

3946
public ConsoleProxyManagementState getManagementState();

server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import org.apache.cloudstack.agent.lb.IndirectAgentLB;
3333
import org.apache.cloudstack.context.CallContext;
3434
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
35+
import org.apache.cloudstack.framework.config.ConfigKey;
36+
import org.apache.cloudstack.framework.config.Configurable;
3537
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
3638
import org.apache.cloudstack.framework.security.keys.KeysManager;
3739
import org.apache.cloudstack.framework.security.keystore.KeystoreDao;
@@ -154,7 +156,8 @@
154156
// Starting, HA, Migrating, Running state are all counted as "Open" for available capacity calculation
155157
// because sooner or later, it will be driven into Running state
156158
//
157-
public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxyManager, VirtualMachineGuru, SystemVmLoadScanHandler<Long>, ResourceStateAdapter {
159+
public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxyManager, VirtualMachineGuru, SystemVmLoadScanHandler<Long>, ResourceStateAdapter, Configurable {
160+
158161
private static final Logger s_logger = Logger.getLogger(ConsoleProxyManagerImpl.class);
159162

160163
private static final int DEFAULT_CAPACITY_SCAN_INTERVAL = 30000; // 30 seconds
@@ -1741,4 +1744,14 @@ public void setConsoleProxyAllocators(List<ConsoleProxyAllocator> consoleProxyAl
17411744
_consoleProxyAllocators = consoleProxyAllocators;
17421745
}
17431746

1747+
@Override
1748+
public String getConfigComponentName() {
1749+
return ConsoleProxyManager.class.getSimpleName();
1750+
}
1751+
1752+
@Override
1753+
public ConfigKey<?>[] getConfigKeys() {
1754+
return new ConfigKey<?>[] { NoVncConsoleDefault };
1755+
}
1756+
17441757
}

server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
import org.springframework.stereotype.Component;
4242
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
4343

44+
45+
import com.cloud.vm.VmDetailConstants;
46+
import com.google.gson.Gson;
47+
import com.google.gson.GsonBuilder;
48+
49+
import com.cloud.consoleproxy.ConsoleProxyManager;
4450
import com.cloud.exception.PermissionDeniedException;
4551
import com.cloud.host.HostVO;
4652
import com.cloud.hypervisor.Hypervisor;
@@ -59,10 +65,7 @@
5965
import com.cloud.vm.UserVmDetailVO;
6066
import com.cloud.vm.VirtualMachine;
6167
import com.cloud.vm.VirtualMachineManager;
62-
import com.cloud.vm.VmDetailConstants;
6368
import com.cloud.vm.dao.UserVmDetailsDao;
64-
import com.google.gson.Gson;
65-
import com.google.gson.GsonBuilder;
6669

6770
/**
6871
* Thumbnail access : /console?cmd=thumbnail&vm=xxx&w=xxx&h=xxx
@@ -478,7 +481,12 @@ private String composeConsoleAccessUrl(String rootUrl, VirtualMachine vm, HostVO
478481
param.setClientTunnelSession(parsedHostInfo.third());
479482
}
480483

481-
sb.append("/ajax?token=" + encryptor.encryptObject(ConsoleProxyClientParam.class, param));
484+
if (param.getHypervHost() != null || !ConsoleProxyManager.NoVncConsoleDefault.value()) {
485+
sb.append("/ajax?token=" + encryptor.encryptObject(ConsoleProxyClientParam.class, param));
486+
} else {
487+
sb.append("/resource/noVNC/vnc.html?port=" + ConsoleProxyManager.DEFAULT_NOVNC_PORT + "&token="
488+
+ encryptor.encryptObject(ConsoleProxyClientParam.class, param));
489+
}
482490

483491
// for console access, we need guest OS type to help implement keyboard
484492
long guestOs = vm.getGuestOSId();

services/console-proxy/server/pom.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@
5050
<artifactId>cloudstack-service-console-proxy-rdpclient</artifactId>
5151
<version>${project.version}</version>
5252
</dependency>
53+
<dependency>
54+
<groupId>javax.websocket</groupId>
55+
<artifactId>javax.websocket-api</artifactId>
56+
<version>1.0</version>
57+
</dependency>
58+
<dependency>
59+
<groupId>org.eclipse.jetty</groupId>
60+
<artifactId>jetty-server</artifactId>
61+
<version>${cs.jetty.version}</version>
62+
</dependency>
63+
<dependency>
64+
<groupId>org.eclipse.jetty.websocket</groupId>
65+
<artifactId>websocket-server</artifactId>
66+
<version>${cs.jetty.version}</version>
67+
</dependency>
5368
</dependencies>
5469
<build>
5570
<resources>

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.concurrent.Executor;
3333

3434
import org.apache.log4j.xml.DOMConfigurator;
35+
import org.eclipse.jetty.websocket.api.Session;
3536

3637
import com.cloud.consoleproxy.util.Logger;
3738
import com.cloud.utils.PropertiesUtil;
@@ -344,6 +345,10 @@ private static void startupHttpMain() {
344345
server.createContext("/ajaximg", new ConsoleProxyAjaxImageHandler());
345346
server.setExecutor(new ThreadExecutor()); // creates a default executor
346347
server.start();
348+
349+
ConsoleProxyNoVNCServer noVNCServer = new ConsoleProxyNoVNCServer(ksBits, ksPassword);
350+
noVNCServer.start();
351+
347352
} catch (Exception e) {
348353
s_logger.error(e.getMessage(), e);
349354
System.exit(1);
@@ -395,7 +400,7 @@ public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) thr
395400
String clientKey = param.getClientMapKey();
396401
synchronized (connectionMap) {
397402
viewer = connectionMap.get(clientKey);
398-
if (viewer == null) {
403+
if (viewer == null || viewer.getClass() == ConsoleProxyNoVncClient.class) {
399404
viewer = getClient(param);
400405
viewer.initClient(param);
401406
connectionMap.put(clientKey, viewer);
@@ -429,7 +434,7 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param,
429434
String clientKey = param.getClientMapKey();
430435
synchronized (connectionMap) {
431436
ConsoleProxyClient viewer = connectionMap.get(clientKey);
432-
if (viewer == null) {
437+
if (viewer == null || viewer.getClass() == ConsoleProxyNoVncClient.class) {
433438
authenticationExternally(param);
434439
viewer = getClient(param);
435440
viewer.initClient(param);
@@ -521,4 +526,48 @@ public void execute(Runnable r) {
521526
new Thread(r).start();
522527
}
523528
}
529+
530+
public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam param, String ajaxSession,
531+
Session session) throws AuthenticationException {
532+
boolean reportLoadChange = false;
533+
String clientKey = param.getClientMapKey();
534+
synchronized (connectionMap) {
535+
ConsoleProxyClient viewer = connectionMap.get(clientKey);
536+
if (viewer == null || viewer.getClass() != ConsoleProxyNoVncClient.class) {
537+
authenticationExternally(param);
538+
viewer = new ConsoleProxyNoVncClient(session);
539+
viewer.initClient(param);
540+
541+
connectionMap.put(clientKey, viewer);
542+
s_logger.info("Added viewer object " + viewer);
543+
reportLoadChange = true;
544+
} else {
545+
// protected against malicous attack by modifying URL content
546+
// if (ajaxSession != null) {
547+
// long ajaxSessionIdFromUrl = Long.parseLong(ajaxSession);
548+
// if (ajaxSessionIdFromUrl != viewer.getAjaxSessionId())
549+
// throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": modified AJAX session id");
550+
// }
551+
552+
if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() ||
553+
!param.getClientHostPassword().equals(viewer.getClientHostPassword()))
554+
throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid");
555+
556+
if (!viewer.isFrontEndAlive()) {
557+
authenticationExternally(param);
558+
viewer.initClient(param);
559+
reportLoadChange = true;
560+
}
561+
}
562+
563+
if (reportLoadChange) {
564+
ConsoleProxyClientStatsCollector statsCollector = getStatsCollector();
565+
String loadInfo = statsCollector.getStatsReport();
566+
reportLoadInfo(loadInfo);
567+
if (s_logger.isDebugEnabled())
568+
s_logger.debug("Report load change : " + loadInfo);
569+
}
570+
return (ConsoleProxyNoVncClient)viewer;
571+
}
572+
}
524573
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package com.cloud.consoleproxy;
18+
19+
import java.io.IOException;
20+
import java.util.Map;
21+
22+
import javax.servlet.ServletException;
23+
import javax.servlet.http.HttpServletRequest;
24+
import javax.servlet.http.HttpServletResponse;
25+
26+
import com.cloud.consoleproxy.util.Logger;
27+
28+
import org.eclipse.jetty.server.Request;
29+
import org.eclipse.jetty.websocket.api.Session;
30+
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
31+
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
32+
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
33+
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
34+
import org.eclipse.jetty.websocket.api.extensions.Frame;
35+
import org.eclipse.jetty.websocket.server.WebSocketHandler;
36+
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
37+
38+
@WebSocket
39+
public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
40+
41+
private ConsoleProxyNoVncClient viewer;
42+
private static final Logger s_logger = Logger.getLogger(ConsoleProxyNoVNCHandler.class);
43+
44+
public ConsoleProxyNoVNCHandler() {
45+
super();
46+
s_logger.info("Default constructor");
47+
}
48+
49+
@Override
50+
public void configure(WebSocketServletFactory webSocketServletFactory) {
51+
webSocketServletFactory.register(ConsoleProxyNoVNCHandler.class);
52+
}
53+
54+
@Override
55+
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
56+
throws IOException, ServletException {
57+
s_logger.info("Hnadling : " + request.getRequestURI());
58+
59+
if (this.getWebSocketFactory().isUpgradeRequest(request, response)) {
60+
s_logger.info("SocketRequest : " + request.getRequestURI());
61+
response.addHeader("Sec-WebSocket-Protocol", "binary");
62+
if (this.getWebSocketFactory().acceptWebSocket(request, response)) {
63+
baseRequest.setHandled(true);
64+
return;
65+
}
66+
67+
if (response.isCommitted()) {
68+
return;
69+
}
70+
}
71+
72+
super.handle(target, baseRequest, request, response);
73+
}
74+
75+
@OnWebSocketConnect
76+
public void onConnect(final Session session) throws IOException, InterruptedException {
77+
s_logger.info("Request : " + session.getUpgradeRequest().getRequestURI() + " : "
78+
+ session.getUpgradeRequest().getParameterMap());
79+
80+
String queries = session.getUpgradeRequest().getQueryString();
81+
Map<String, String> queryMap = ConsoleProxyHttpHandlerHelper.getQueryMap(queries);
82+
83+
String host = queryMap.get("host");
84+
String portStr = queryMap.get("port");
85+
String sid = queryMap.get("sid");
86+
String tag = queryMap.get("tag");
87+
String ticket = queryMap.get("ticket");
88+
String ajaxSessionIdStr = queryMap.get("sess");
89+
String console_url = queryMap.get("consoleurl");
90+
String console_host_session = queryMap.get("sessionref");
91+
String vm_locale = queryMap.get("locale");
92+
String hypervHost = queryMap.get("hypervHost");
93+
String username = queryMap.get("username");
94+
String password = queryMap.get("password");
95+
96+
if (tag == null)
97+
tag = "";
98+
99+
long ajaxSessionId = 0;
100+
int port;
101+
102+
if (host == null || portStr == null || sid == null)
103+
throw new IllegalArgumentException();
104+
105+
try {
106+
port = Integer.parseInt(portStr);
107+
} catch (NumberFormatException e) {
108+
s_logger.warn("Invalid number parameter in query string: " + portStr);
109+
throw new IllegalArgumentException(e);
110+
}
111+
112+
if (ajaxSessionIdStr != null) {
113+
try {
114+
ajaxSessionId = Long.parseLong(ajaxSessionIdStr);
115+
} catch (NumberFormatException e) {
116+
s_logger.warn("Invalid number parameter in query string: " + ajaxSessionIdStr);
117+
throw new IllegalArgumentException(e);
118+
}
119+
}
120+
121+
try {
122+
ConsoleProxyClientParam param = new ConsoleProxyClientParam();
123+
param.setClientHostAddress(host);
124+
param.setClientHostPort(port);
125+
param.setClientHostPassword(sid);
126+
param.setClientTag(tag);
127+
param.setTicket(ticket);
128+
param.setClientTunnelUrl(console_url);
129+
param.setClientTunnelSession(console_host_session);
130+
param.setLocale(vm_locale);
131+
param.setHypervHost(hypervHost);
132+
param.setUsername(username);
133+
param.setPassword(password);
134+
135+
s_logger.info("Getting viewer");
136+
viewer = ConsoleProxy.getNoVncViewer(param, ajaxSessionIdStr, session);
137+
} catch (Exception e) {
138+
s_logger.warn("Failed to create viewer due to " + e.getMessage(), e);
139+
return;
140+
}
141+
142+
// if (ajaxSessionId == 0 || ajaxSessionId != viewer.getAjaxSessionId()) {
143+
// if (s_logger.isDebugEnabled())
144+
// s_logger.debug("Ajax request comes from a different session, id in request: " + ajaxSessionId
145+
// + ", id in viewer: " + viewer.getAjaxSessionId());
146+
// session.getRemote().sendBytes(ByteBuffer.wrap("Invalid ajax client session id".getBytes()));
147+
// }
148+
149+
s_logger.info("Got viewer");
150+
}
151+
152+
@OnWebSocketClose
153+
public void onClose(Session session, int statusCode, String reason) throws IOException, InterruptedException {
154+
s_logger.info("Closing");
155+
ConsoleProxy.removeViewer(viewer);
156+
}
157+
158+
@OnWebSocketFrame
159+
public void onFrame(Frame f) throws IOException {
160+
viewer.sendClientFrame(f);
161+
}
162+
}

0 commit comments

Comments
 (0)