mirror of https://github.com/apache/cloudstack.git
Fix console proxy idle timeout and noVNC session handling
This commit is contained in:
parent
744bb6542e
commit
6eef242d21
File diff suppressed because it is too large
Load Diff
|
|
@ -1,160 +1,140 @@
|
|||
// 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.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* ConsoleProxyGCThread does house-keeping work for the process, it helps cleanup log files,
|
||||
* recycle idle client sessions without front-end activities and report client stats to external
|
||||
* management software
|
||||
*/
|
||||
public class ConsoleProxyGCThread extends Thread {
|
||||
protected Logger logger = LogManager.getLogger(ConsoleProxyGCThread.class);
|
||||
|
||||
|
||||
private final static int DEFAULT_MAX_SESSION_IDLE_SECONDS = 180;
|
||||
|
||||
|
||||
private final Map<String, ConsoleProxyClient> connMap;
|
||||
private final Set<String> removedSessionsSet;
|
||||
private long lastLogScan = 0;
|
||||
|
||||
|
||||
public ConsoleProxyGCThread(Map<String, ConsoleProxyClient> connMap, Set<String> removedSet) {
|
||||
this.connMap = connMap;
|
||||
this.removedSessionsSet = removedSet;
|
||||
}
|
||||
|
||||
|
||||
private int getMaxSessionIdleSeconds() {
|
||||
if (ConsoleProxy.sessionTimeoutMillis <= 0) {
|
||||
return DEFAULT_MAX_SESSION_IDLE_SECONDS;
|
||||
}
|
||||
return Math.max(1, ConsoleProxy.sessionTimeoutMillis / 1000);
|
||||
}
|
||||
|
||||
|
||||
private void cleanupLogging() {
|
||||
if (lastLogScan != 0 && System.currentTimeMillis() - lastLogScan < 3600000)
|
||||
return;
|
||||
|
||||
|
||||
lastLogScan = System.currentTimeMillis();
|
||||
|
||||
|
||||
File logDir = new File("./logs");
|
||||
File files[] = logDir.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (System.currentTimeMillis() - file.lastModified() >= 86400000L) {
|
||||
try {
|
||||
file.delete();
|
||||
} catch (Throwable e) {
|
||||
logger.info("[ignored]"
|
||||
+ "failed to delete file: " + e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
|
||||
boolean bReportLoad = false;
|
||||
long lastReportTick = System.currentTimeMillis();
|
||||
|
||||
|
||||
while (true) {
|
||||
cleanupLogging();
|
||||
bReportLoad = false;
|
||||
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("connMap=%s, removedSessions=%s", connMap, removedSessionsSet));
|
||||
}
|
||||
Set<String> e = connMap.keySet();
|
||||
Iterator<String> iterator = e.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
String key;
|
||||
ConsoleProxyClient client;
|
||||
|
||||
|
||||
synchronized (connMap) {
|
||||
key = iterator.next();
|
||||
client = connMap.get(key);
|
||||
}
|
||||
|
||||
|
||||
long seconds_unused = (System.currentTimeMillis() - client.getClientLastFrontEndActivityTime()) / 1000;
|
||||
if (seconds_unused < getMaxSessionIdleSeconds()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
synchronized (connMap) {
|
||||
connMap.remove(key);
|
||||
bReportLoad = true;
|
||||
}
|
||||
|
||||
|
||||
// close the server connection
|
||||
logger.info("Dropping " + client + " which has not been used for " + seconds_unused + " seconds");
|
||||
client.closeClient();
|
||||
}
|
||||
|
||||
|
||||
if (bReportLoad || System.currentTimeMillis() - lastReportTick > 5000) {
|
||||
// report load changes
|
||||
ConsoleProxyClientStatsCollector collector = new ConsoleProxyClientStatsCollector(connMap);
|
||||
collector.setRemovedSessions(new ArrayList<>(removedSessionsSet));
|
||||
String loadInfo = collector.getStatsReport();
|
||||
ConsoleProxy.reportLoadInfo(loadInfo);
|
||||
lastReportTick = System.currentTimeMillis();
|
||||
synchronized (removedSessionsSet) {
|
||||
removedSessionsSet.clear();
|
||||
}
|
||||
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Report load change : " + loadInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException ex) {
|
||||
logger.debug("[ignored] Console proxy was interrupted during GC.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
|
||||
/**
|
||||
*
|
||||
* ConsoleProxyGCThread does house-keeping work for the process, it helps cleanup log files,
|
||||
* recycle idle client sessions without front-end activities and report client stats to external
|
||||
* management software
|
||||
*/
|
||||
public class ConsoleProxyGCThread extends Thread {
|
||||
private static final Logger logger = LogManager.getLogger(ConsoleProxyGCThread.class);
|
||||
|
||||
/**
|
||||
* Maximum time (in seconds) a console session is allowed to be idle before it is closed.
|
||||
* This value should be kept in sync with ConsoleProxy.VIEWER_LINGER_SECONDS.
|
||||
*/
|
||||
private static final int MAX_SESSION_IDLE_SECONDS = 180;
|
||||
|
||||
private final Map<String, ConsoleProxyClient> connMap;
|
||||
private final Set<String> removedSessionsSet;
|
||||
private long lastLogScan = 0;
|
||||
|
||||
public ConsoleProxyGCThread(Map<String, ConsoleProxyClient> connMap, Set<String> removedSet) {
|
||||
this.connMap = connMap;
|
||||
this.removedSessionsSet = removedSet;
|
||||
}
|
||||
|
||||
private void cleanupLogging() {
|
||||
if (lastLogScan != 0 && System.currentTimeMillis() - lastLogScan < 3600000) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastLogScan = System.currentTimeMillis();
|
||||
|
||||
File logDir = new File("./logs");
|
||||
File[] files = logDir.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (System.currentTimeMillis() - file.lastModified() >= 86400000L) {
|
||||
try {
|
||||
file.delete();
|
||||
} catch (Throwable e) {
|
||||
logger.info("[ignored] failed to delete file: " + e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
boolean bReportLoad = false;
|
||||
long lastReportTick = System.currentTimeMillis();
|
||||
|
||||
while (true) {
|
||||
cleanupLogging();
|
||||
bReportLoad = false;
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("ConsoleProxyGCThread loop: connMap=%s, removedSessions=%s", connMap, removedSessionsSet));
|
||||
}
|
||||
Set<String> keys = connMap.keySet();
|
||||
Iterator<String> iterator = keys.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
String key;
|
||||
ConsoleProxyClient client;
|
||||
|
||||
synchronized (connMap) {
|
||||
key = iterator.next();
|
||||
client = connMap.get(key);
|
||||
}
|
||||
|
||||
if (client == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long secondsUnused = (System.currentTimeMillis() - client.getClientLastFrontEndActivityTime()) / 1000;
|
||||
if (secondsUnused < MAX_SESSION_IDLE_SECONDS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
synchronized (connMap) {
|
||||
connMap.remove(key);
|
||||
bReportLoad = true;
|
||||
}
|
||||
|
||||
// close the server connection
|
||||
logger.info("Dropping " + client + " which has not been used for " + secondsUnused + " seconds");
|
||||
client.closeClient();
|
||||
}
|
||||
|
||||
if (bReportLoad || System.currentTimeMillis() - lastReportTick > 5000) {
|
||||
// report load changes, including removed sessions since last report
|
||||
ConsoleProxyClientStatsCollector collector = new ConsoleProxyClientStatsCollector(connMap);
|
||||
collector.setRemovedSessions(new ArrayList<>(removedSessionsSet));
|
||||
String loadInfo = collector.getStatsReport();
|
||||
ConsoleProxy.reportLoadInfo(loadInfo);
|
||||
lastReportTick = System.currentTimeMillis();
|
||||
synchronized (removedSessionsSet) {
|
||||
removedSessionsSet.clear();
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Report load change : " + loadInfo);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException ex) {
|
||||
logger.debug("[ignored] Console proxy GC thread interrupted.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,240 +1,206 @@
|
|||
// 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.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.api.extensions.Frame;
|
||||
import org.eclipse.jetty.websocket.server.WebSocketHandler;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
|
||||
|
||||
@WebSocket
|
||||
public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
|
||||
|
||||
|
||||
private ConsoleProxyNoVncClient viewer = null;
|
||||
protected Logger logger = LogManager.getLogger(getClass());
|
||||
|
||||
|
||||
public ConsoleProxyNoVNCHandler() {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory webSocketServletFactory) {
|
||||
webSocketServletFactory.register(ConsoleProxyNoVNCHandler.class);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException, ServletException {
|
||||
|
||||
|
||||
if (this.getWebSocketFactory().isUpgradeRequest(request, response)) {
|
||||
response.addHeader("Sec-WebSocket-Protocol", "binary");
|
||||
if (this.getWebSocketFactory().acceptWebSocket(request, response)) {
|
||||
baseRequest.setHandled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (response.isCommitted()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
super.handle(target, baseRequest, request, response);
|
||||
}
|
||||
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onConnect(final Session session) throws IOException, InterruptedException {
|
||||
String queries = session.getUpgradeRequest().getQueryString();
|
||||
Map<String, String> queryMap = ConsoleProxyHttpHandlerHelper.getQueryMap(queries);
|
||||
|
||||
|
||||
String host = queryMap.get("host");
|
||||
String portStr = queryMap.get("port");
|
||||
String sid = queryMap.get("sid");
|
||||
String tag = queryMap.get("tag");
|
||||
String ticket = queryMap.get("ticket");
|
||||
String displayName = queryMap.get("displayname");
|
||||
String ajaxSessionIdStr = queryMap.get("sess");
|
||||
String consoleUrl = queryMap.get("consoleurl");
|
||||
String consoleHostSession = queryMap.get("sessionref");
|
||||
String vmLocale = queryMap.get("locale");
|
||||
String hypervHost = queryMap.get("hypervHost");
|
||||
String username = queryMap.get("username");
|
||||
String password = queryMap.get("password");
|
||||
String sourceIP = queryMap.get("sourceIP");
|
||||
String websocketUrl = queryMap.get("websocketUrl");
|
||||
String sessionUuid = queryMap.get("sessionUuid");
|
||||
String clientIp = session.getRemoteAddress().getAddress().getHostAddress();
|
||||
boolean sessionRequiresNewViewer = Boolean.parseBoolean(queryMap.get("sessionRequiresNewViewer"));
|
||||
|
||||
|
||||
if (tag == null)
|
||||
tag = "";
|
||||
|
||||
|
||||
long ajaxSessionId = 0;
|
||||
int port;
|
||||
|
||||
|
||||
if (host == null || portStr == null || sid == null)
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
|
||||
try {
|
||||
port = Integer.parseInt(portStr);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.error("Invalid port value in query string: {}. Expected a number.", portStr, e);
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
|
||||
if (ajaxSessionIdStr != null) {
|
||||
try {
|
||||
ajaxSessionId = Long.parseLong(ajaxSessionIdStr);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.error("Invalid ajaxSessionId (sess) value in query string: {}. Expected a number.", ajaxSessionIdStr, e);
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!checkSessionSourceIp(session, sourceIP, clientIp)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
if (ConsoleProxy.sessionTimeoutMillis > 0) {
|
||||
session.setIdleTimeout(ConsoleProxy.sessionTimeoutMillis);
|
||||
logger.debug("Set noVNC WebSocket idle timeout to {} ms for session UUID: {}.",
|
||||
ConsoleProxy.sessionTimeoutMillis, sessionUuid);
|
||||
} else {
|
||||
logger.debug("Using default noVNC WebSocket idle timeout for session UUID: {}.", sessionUuid);
|
||||
}
|
||||
|
||||
|
||||
ConsoleProxyClientParam param = new ConsoleProxyClientParam();
|
||||
param.setClientHostAddress(host);
|
||||
param.setClientHostPort(port);
|
||||
param.setClientHostPassword(sid);
|
||||
param.setClientTag(tag);
|
||||
param.setTicket(ticket);
|
||||
param.setClientDisplayName(displayName);
|
||||
param.setClientTunnelUrl(consoleUrl);
|
||||
param.setClientTunnelSession(consoleHostSession);
|
||||
param.setLocale(vmLocale);
|
||||
param.setHypervHost(hypervHost);
|
||||
param.setUsername(username);
|
||||
param.setPassword(password);
|
||||
param.setWebsocketUrl(websocketUrl);
|
||||
param.setSessionUuid(sessionUuid);
|
||||
param.setSourceIP(sourceIP);
|
||||
param.setClientIp(clientIp);
|
||||
param.setSessionRequiresNewViewer(sessionRequiresNewViewer);
|
||||
|
||||
|
||||
if (queryMap.containsKey("extraSecurityToken")) {
|
||||
param.setExtraSecurityToken(queryMap.get("extraSecurityToken"));
|
||||
}
|
||||
if (queryMap.containsKey("extra")) {
|
||||
param.setClientProvidedExtraSecurityToken(queryMap.get("extra"));
|
||||
}
|
||||
viewer = ConsoleProxy.getNoVncViewer(param, ajaxSessionIdStr, session);
|
||||
logger.info("Viewer has been created successfully [session UUID: {}, client IP: {}].", sessionUuid, clientIp);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to create viewer [session UUID: {}, client IP: {}] due to {}.", sessionUuid, clientIp, e.getMessage(), e);
|
||||
return;
|
||||
} finally {
|
||||
if (viewer == null) {
|
||||
session.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean checkSessionSourceIp(final Session session, final String sourceIP, String sessionSourceIP) throws IOException {
|
||||
logger.info("Verifying session source IP {} from WebSocket connection request.", sessionSourceIP);
|
||||
if (ConsoleProxy.isSourceIpCheckEnabled && (sessionSourceIP == null || !sessionSourceIP.equals(sourceIP))) {
|
||||
logger.warn("Failed to access console as the source IP to request the console is {}.", sourceIP);
|
||||
session.disconnect();
|
||||
return false;
|
||||
}
|
||||
logger.debug("Session source IP {} has been verified successfully.", sessionSourceIP);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onClose(Session session, int statusCode, String reason) throws IOException, InterruptedException {
|
||||
String sessionSourceIp = session.getRemoteAddress().getAddress().getHostAddress();
|
||||
logger.debug("Closing WebSocket session [source IP: {}, status code: {}].", sessionSourceIp, statusCode);
|
||||
if (viewer != null) {
|
||||
ConsoleProxy.removeViewer(viewer);
|
||||
}
|
||||
logger.debug("WebSocket session [source IP: {}, status code: {}] closed successfully.", sessionSourceIp, statusCode);
|
||||
}
|
||||
|
||||
|
||||
@OnWebSocketFrame
|
||||
public void onFrame(Frame f) throws IOException {
|
||||
if (viewer == null) {
|
||||
logger.warn("Ignoring WebSocket frame because viewer is not initialized yet.");
|
||||
return;
|
||||
}
|
||||
logger.trace("Sending client [ID: {}] frame of {} bytes.", viewer.getClientId(), f.getPayloadLength());
|
||||
viewer.sendClientFrame(f);
|
||||
}
|
||||
|
||||
|
||||
@OnWebSocketError
|
||||
public void onError(Throwable cause) {
|
||||
if (viewer != null) {
|
||||
logger.error("Error on WebSocket [client ID: {}, session UUID: {}].",
|
||||
viewer.getClientId(), viewer.getSessionUuid(), cause);
|
||||
} else {
|
||||
logger.error("Error on WebSocket before viewer initialization.", cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.api.extensions.Frame;
|
||||
import org.eclipse.jetty.websocket.server.WebSocketHandler;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
|
||||
@WebSocket
|
||||
public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(ConsoleProxyNoVNCHandler.class);
|
||||
|
||||
private ConsoleProxyNoVncClient viewer = null;
|
||||
|
||||
public ConsoleProxyNoVNCHandler() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory webSocketServletFactory) {
|
||||
webSocketServletFactory.register(ConsoleProxyNoVNCHandler.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException, ServletException {
|
||||
|
||||
if (this.getWebSocketFactory().isUpgradeRequest(request, response)) {
|
||||
response.addHeader("Sec-WebSocket-Protocol", "binary");
|
||||
if (this.getWebSocketFactory().acceptWebSocket(request, response)) {
|
||||
baseRequest.setHandled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.isCommitted()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
super.handle(target, baseRequest, request, response);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onConnect(final Session session) throws IOException, InterruptedException {
|
||||
String queries = session.getUpgradeRequest().getQueryString();
|
||||
Map<String, String> queryMap = ConsoleProxyHttpHandlerHelper.getQueryMap(queries);
|
||||
|
||||
String host = queryMap.get("host");
|
||||
String portStr = queryMap.get("port");
|
||||
String sid = queryMap.get("sid");
|
||||
String tag = queryMap.get("tag");
|
||||
String ticket = queryMap.get("ticket");
|
||||
String displayName = queryMap.get("displayname");
|
||||
String ajaxSessionIdStr = queryMap.get("sess");
|
||||
String consoleUrl = queryMap.get("consoleurl");
|
||||
String consoleHostSession = queryMap.get("sessionref");
|
||||
String vmLocale = queryMap.get("locale");
|
||||
String hypervHost = queryMap.get("hypervHost");
|
||||
String username = queryMap.get("username");
|
||||
String password = queryMap.get("password");
|
||||
String sourceIP = queryMap.get("sourceIP");
|
||||
String websocketUrl = queryMap.get("websocketUrl");
|
||||
String sessionUuid = queryMap.get("sessionUuid");
|
||||
String clientIp = session.getRemoteAddress().getAddress().getHostAddress();
|
||||
boolean sessionRequiresNewViewer = Boolean.parseBoolean(queryMap.get("sessionRequiresNewViewer"));
|
||||
|
||||
if (tag == null) {
|
||||
tag = "";
|
||||
}
|
||||
|
||||
int port;
|
||||
if (host == null || portStr == null || sid == null) {
|
||||
throw new IllegalArgumentException("Missing required console connection parameters");
|
||||
}
|
||||
|
||||
try {
|
||||
port = Integer.parseInt(portStr);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.error("Invalid port value in query string: {}. Expected a number.", portStr, e);
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
if (ajaxSessionIdStr != null) {
|
||||
try {
|
||||
Long.parseLong(ajaxSessionIdStr);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.error("Invalid ajaxSessionId (sess) value in query string: {}. Expected a number.", ajaxSessionIdStr, e);
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!checkSessionSourceIp(session, sourceIP, clientIp)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ConsoleProxyClientParam param = new ConsoleProxyClientParam();
|
||||
param.setClientHostAddress(host);
|
||||
param.setClientHostPort(port);
|
||||
param.setClientHostPassword(sid);
|
||||
param.setClientTag(tag);
|
||||
param.setTicket(ticket);
|
||||
param.setClientDisplayName(displayName);
|
||||
param.setClientTunnelUrl(consoleUrl);
|
||||
param.setClientTunnelSession(consoleHostSession);
|
||||
param.setLocale(vmLocale);
|
||||
param.setHypervHost(hypervHost);
|
||||
param.setUsername(username);
|
||||
param.setPassword(password);
|
||||
param.setWebsocketUrl(websocketUrl);
|
||||
param.setSessionUuid(sessionUuid);
|
||||
param.setSourceIP(sourceIP);
|
||||
param.setClientIp(clientIp);
|
||||
param.setSessionRequiresNewViewer(sessionRequiresNewViewer);
|
||||
|
||||
if (queryMap.containsKey("extraSecurityToken")) {
|
||||
param.setExtraSecurityToken(queryMap.get("extraSecurityToken"));
|
||||
}
|
||||
if (queryMap.containsKey("extra")) {
|
||||
param.setClientProvidedExtraSecurityToken(queryMap.get("extra"));
|
||||
}
|
||||
|
||||
viewer = ConsoleProxy.getNoVncViewer(param, ajaxSessionIdStr, session);
|
||||
logger.info("Viewer has been created successfully [session UUID: {}, client IP: {}].", sessionUuid, clientIp);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to create viewer [session UUID: {}, client IP: {}].", sessionUuid, clientIp, e);
|
||||
return;
|
||||
} finally {
|
||||
if (viewer == null) {
|
||||
session.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkSessionSourceIp(final Session session, final String sourceIP, final String sessionSourceIP) throws IOException {
|
||||
logger.info("Verifying session source IP {} from WebSocket connection request.", sessionSourceIP);
|
||||
if (ConsoleProxy.isSourceIpCheckEnabled && (sessionSourceIP == null || !sessionSourceIP.equals(sourceIP))) {
|
||||
logger.warn("Failed to access console as the source IP to request the console is {}.", sourceIP);
|
||||
session.disconnect();
|
||||
return false;
|
||||
}
|
||||
logger.debug("Session source IP {} has been verified successfully.", sessionSourceIP);
|
||||
return true;
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onClose(Session session, int statusCode, String reason) throws IOException, InterruptedException {
|
||||
String sessionSourceIp = session.getRemoteAddress().getAddress().getHostAddress();
|
||||
logger.debug("Closing WebSocket session [source IP: {}, status code: {}, reason: {}].", sessionSourceIp, statusCode, reason);
|
||||
if (viewer != null) {
|
||||
ConsoleProxy.removeViewer(viewer);
|
||||
}
|
||||
logger.debug("WebSocket session [source IP: {}, status code: {}] closed successfully.", sessionSourceIp, statusCode);
|
||||
}
|
||||
|
||||
@OnWebSocketFrame
|
||||
public void onFrame(Frame f) throws IOException {
|
||||
if (viewer == null) {
|
||||
logger.debug("Ignoring WebSocket frame because viewer is not initialized yet.");
|
||||
return;
|
||||
}
|
||||
logger.trace("Sending client [ID: {}] frame of {} bytes.", viewer.getClientId(), f.getPayloadLength());
|
||||
viewer.sendClientFrame(f);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onError(Throwable cause) {
|
||||
if (viewer != null) {
|
||||
logger.error("Error on WebSocket [client ID: {}, session UUID: {}].", viewer.getClientId(), viewer.getSessionUuid(), cause);
|
||||
} else {
|
||||
logger.error("Error on WebSocket before viewer initialization.", cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue