Fix console proxy idle timeout and noVNC session handling

This commit is contained in:
dheeraj12347 2026-04-19 11:39:06 +05:30
parent 744bb6542e
commit 6eef242d21
3 changed files with 1009 additions and 1065 deletions

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}