This commit is contained in:
Dheeraj Bansal 2026-05-12 08:17:09 +01:00 committed by GitHub
commit e08a8302c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 108 additions and 71 deletions

View File

@ -81,7 +81,7 @@ public class ConsoleProxy {
static boolean standaloneStart = false;
static String encryptorPassword = "Dummy";
static final String[] skipProperties = new String[]{"certificate", "cacertificate", "keystore_password", "privatekey"};
static final String[] skipProperties = new String[] {"certificate", "cacertificate", "keystore_password", "privatekey"};
static Set<String> allowedSessions = new HashSet<>();
@ -92,11 +92,13 @@ public class ConsoleProxy {
private static void configLog4j() {
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
URL configUrl = loader.getResource("/conf/log4j-cloud.xml");
if (configUrl == null)
if (configUrl == null) {
configUrl = ClassLoader.getSystemResource("log4j-cloud.xml");
}
if (configUrl == null)
if (configUrl == null) {
configUrl = ClassLoader.getSystemResource("conf/log4j-cloud.xml");
}
if (configUrl != null) {
try {
@ -120,20 +122,21 @@ public class ConsoleProxy {
private static void configProxy(Properties conf) {
LOGGER.info("Configure console proxy...");
for (Object key : conf.keySet()) {
LOGGER.info("Property " + (String)key + ": " + conf.getProperty((String)key));
if (!ArrayUtils.contains(skipProperties, key)) {
LOGGER.info("Property " + (String)key + ": " + conf.getProperty((String)key));
if (conf != null) {
for (Object key : conf.keySet()) {
if (!ArrayUtils.contains(skipProperties, key)) {
LOGGER.info("Property " + (String) key + ": " + conf.getProperty((String) key));
}
}
}
String s = conf.getProperty("consoleproxy.httpListenPort");
String s = conf != null ? conf.getProperty("consoleproxy.httpListenPort") : null;
if (s != null) {
httpListenPort = Integer.parseInt(s);
LOGGER.info("Setting httpListenPort=" + s);
}
s = conf.getProperty("premium");
s = conf != null ? conf.getProperty("premium") : null;
if (s != null && s.equalsIgnoreCase("true")) {
LOGGER.info("Premium setting will override settings from consoleproxy.properties, listen at port 443");
httpListenPort = 443;
@ -142,25 +145,25 @@ public class ConsoleProxy {
factoryClzName = ConsoleProxyBaseServerFactoryImpl.class.getName();
}
s = conf.getProperty("consoleproxy.httpCmdListenPort");
s = conf != null ? conf.getProperty("consoleproxy.httpCmdListenPort") : null;
if (s != null) {
httpCmdListenPort = Integer.parseInt(s);
LOGGER.info("Setting httpCmdListenPort=" + s);
}
s = conf.getProperty("consoleproxy.reconnectMaxRetry");
s = conf != null ? conf.getProperty("consoleproxy.reconnectMaxRetry") : null;
if (s != null) {
reconnectMaxRetry = Integer.parseInt(s);
LOGGER.info("Setting reconnectMaxRetry=" + reconnectMaxRetry);
}
s = conf.getProperty("consoleproxy.readTimeoutSeconds");
s = conf != null ? conf.getProperty("consoleproxy.readTimeoutSeconds") : null;
if (s != null) {
readTimeoutSeconds = Integer.parseInt(s);
LOGGER.info("Setting readTimeoutSeconds=" + readTimeoutSeconds);
}
s = conf.getProperty("consoleproxy.defaultBufferSize");
s = conf != null ? conf.getProperty("consoleproxy.defaultBufferSize") : null;
if (s != null) {
defaultBufferSize = Integer.parseInt(s);
LOGGER.info("Setting defaultBufferSize=" + defaultBufferSize);
@ -171,7 +174,7 @@ public class ConsoleProxy {
try {
Class<?> clz = Class.forName(factoryClzName);
try {
ConsoleProxyServerFactory factory = (ConsoleProxyServerFactory)clz.newInstance();
ConsoleProxyServerFactory factory = (ConsoleProxyServerFactory) clz.newInstance();
factory.init(ConsoleProxy.ksBits, ConsoleProxy.ksPassword);
return factory;
} catch (InstantiationException e) {
@ -194,11 +197,11 @@ public class ConsoleProxy {
authResult.setHost(param.getClientHostAddress());
authResult.setPort(param.getClientHostPort());
if (org.apache.commons.lang3.StringUtils.isNotBlank(param.getExtraSecurityToken())) {
if (StringUtils.isNotBlank(param.getExtraSecurityToken())) {
String extraToken = param.getExtraSecurityToken();
String clientProvidedToken = param.getClientProvidedExtraSecurityToken();
LOGGER.debug(String.format("Extra security validation for the console access, provided %s " +
"to validate against %s", clientProvidedToken, extraToken));
LOGGER.debug(String.format("Extra security validation for the console access, provided %s to validate against %s",
clientProvidedToken, extraToken));
if (!extraToken.equals(clientProvidedToken)) {
LOGGER.error("The provided extra token does not match the expected value for this console endpoint");
@ -230,20 +233,21 @@ public class ConsoleProxy {
Object result;
try {
result =
authMethod.invoke(ConsoleProxy.context, param.getClientHostAddress(), String.valueOf(param.getClientHostPort()), param.getClientTag(),
param.getClientHostPassword(), param.getTicket(), reauthentication, param.getSessionUuid(), param.getClientIp());
authMethod.invoke(ConsoleProxy.context, param.getClientHostAddress(), String.valueOf(param.getClientHostPort()),
param.getClientTag(), param.getClientHostPassword(), param.getTicket(), reauthentication,
param.getSessionUuid(), param.getClientIp());
} catch (IllegalAccessException e) {
LOGGER.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException" + " for vm: " + param.getClientTag(), e);
LOGGER.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException for vm: " + param.getClientTag(), e);
authResult.setSuccess(false);
return authResult;
} catch (InvocationTargetException e) {
LOGGER.error("Unable to invoke authenticateConsoleAccess due to InvocationTargetException " + " for vm: " + param.getClientTag(), e);
LOGGER.error("Unable to invoke authenticateConsoleAccess due to InvocationTargetException for vm: " + param.getClientTag(), e);
authResult.setSuccess(false);
return authResult;
}
if (result != null && result instanceof String) {
authResult = new Gson().fromJson((String)result, ConsoleProxyAuthenticationResult.class);
authResult = new Gson().fromJson((String) result, ConsoleProxyAuthenticationResult.class);
} else {
LOGGER.error("Invalid authentication return object " + result + " for vm: " + param.getClientTag() + ", decline the access");
authResult.setSuccess(false);
@ -283,7 +287,8 @@ public class ConsoleProxy {
}
}
public static void startWithContext(Properties conf, Object context, byte[] ksBits, String ksPassword, String password, Boolean isSourceIpCheckEnabled) {
public static void startWithContext(Properties conf, Object context, byte[] ksBits, String ksPassword,
String password, Boolean isSourceIpCheckEnabled) {
setEncryptorPassword(password);
configLog4j();
LOGGER.info("Start console proxy with context");
@ -323,34 +328,40 @@ public class ConsoleProxy {
Properties props = new Properties();
if (confs == null) {
final File file = PropertiesUtil.findConfigFile("consoleproxy.properties");
if (file == null)
if (file == null) {
LOGGER.info("Can't load consoleproxy.properties from classpath, will use default configuration");
else
} else {
try {
confs = new FileInputStream(file);
} catch (FileNotFoundException e) {
LOGGER.info("Ignoring file not found exception and using defaults");
}
}
}
if (confs != null) {
try {
props.load(confs);
if (conf == null) {
conf = new Properties();
}
for (Object key : props.keySet()) {
// give properties passed via context high priority, treat properties from consoleproxy.properties
// as default values
if (conf.get(key) == null)
if (conf.get(key) == null) {
conf.put(key, props.get(key));
}
}
} catch (Exception e) {
LOGGER.error(e.toString(), e);
} finally {
try {
confs.close();
} catch (IOException e) {
LOGGER.error("Failed to close consoleproxy.properties : " + e.toString(), e);
}
}
}
try {
confs.close();
} catch (IOException e) {
LOGGER.error("Failed to close consolepropxy.properties : " + e.toString(), e);
}
start(conf);
}
@ -471,8 +482,8 @@ public class ConsoleProxy {
LOGGER.info("The rfb thread died, reinitializing the viewer " + viewer);
viewer.initClient(param);
} else if (!param.getClientHostPassword().equals(viewer.getClientHostPassword())) {
LOGGER.warn("Bad sid detected(VNC port may be reused). sid in session: " + viewer.getClientHostPassword() + ", sid in request: " +
param.getClientHostPassword());
LOGGER.warn("Bad sid detected(VNC port may be reused). sid in session: " + viewer.getClientHostPassword() +
", sid in request: " + param.getClientHostPassword());
viewer.initClient(param);
}
}
@ -481,8 +492,9 @@ public class ConsoleProxy {
ConsoleProxyClientStatsCollector statsCollector = getStatsCollector();
String loadInfo = statsCollector.getStatsReport();
reportLoadInfo(loadInfo);
if (LOGGER.isDebugEnabled())
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Report load change : " + loadInfo);
}
}
return viewer;
@ -506,13 +518,15 @@ public class ConsoleProxy {
// protected against malicious attack by modifying URL content
if (ajaxSession != null) {
long ajaxSessionIdFromUrl = Long.parseLong(ajaxSession);
if (ajaxSessionIdFromUrl != viewer.getAjaxSessionId())
if (ajaxSessionIdFromUrl != viewer.getAjaxSessionId()) {
throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": modified AJAX session id");
}
}
if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() ||
!param.getClientHostPassword().equals(viewer.getClientHostPassword()))
if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty()
|| !param.getClientHostPassword().equals(viewer.getClientHostPassword())) {
throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid");
}
if (!viewer.isFrontEndAlive()) {
@ -526,8 +540,9 @@ public class ConsoleProxy {
ConsoleProxyClientStatsCollector statsCollector = getStatsCollector();
String loadInfo = statsCollector.getStatsReport();
reportLoadInfo(loadInfo);
if (LOGGER.isDebugEnabled())
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Report load change : " + loadInfo);
}
}
return viewer;
}
@ -563,9 +578,11 @@ public class ConsoleProxy {
ConsoleProxyAuthenticationResult authResult = authenticateConsoleAccess(param, false);
if (authResult == null || !authResult.isSuccess()) {
LOGGER.warn("External authenticator failed authentication request for vm " + param.getClientTag() + " with sid " + param.getClientHostPassword());
LOGGER.warn("External authenticator failed authentication request for vm " + param.getClientTag()
+ " with sid " + param.getClientHostPassword());
throw new AuthenticationException("External authenticator failed request for vm " + param.getClientTag() + " with sid " + param.getClientHostPassword());
throw new AuthenticationException("External authenticator failed request for vm " + param.getClientTag()
+ " with sid " + param.getClientHostPassword());
}
}
@ -593,7 +610,7 @@ public class ConsoleProxy {
}
public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam param, String ajaxSession,
Session session) throws AuthenticationException {
Session session) throws AuthenticationException {
boolean reportLoadChange = false;
String clientKey = param.getClientMapKey();
LOGGER.debug("Getting NoVNC viewer for {}. Session requires new viewer: {}, client tag: {}. session UUID: {}",
@ -608,9 +625,10 @@ public class ConsoleProxy {
connectionMap.put(clientKey, viewer);
reportLoadChange = true;
} else {
if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() ||
!param.getClientHostPassword().equals(viewer.getClientHostPassword()))
if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty()
|| !param.getClientHostPassword().equals(viewer.getClientHostPassword())) {
throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid");
}
try {
authenticationExternally(param);
@ -620,7 +638,7 @@ public class ConsoleProxy {
}
LOGGER.info("Initializing new novnc client and disconnecting existing session");
try {
((ConsoleProxyNoVncClient)viewer).getSession().disconnect();
((ConsoleProxyNoVncClient) viewer).getSession().disconnect();
} catch (IOException e) {
LOGGER.error("Exception while disconnect session of novnc viewer object: " + viewer, e);
}
@ -635,10 +653,11 @@ public class ConsoleProxy {
ConsoleProxyClientStatsCollector statsCollector = getStatsCollector();
String loadInfo = statsCollector.getStatsReport();
reportLoadInfo(loadInfo);
if (LOGGER.isDebugEnabled())
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Report load change : " + loadInfo);
}
}
return (ConsoleProxyNoVncClient)viewer;
return (ConsoleProxyNoVncClient) viewer;
}
}
}

View File

@ -32,9 +32,13 @@ import org.apache.logging.log4j.LogManager;
* management software
*/
public class ConsoleProxyGCThread extends Thread {
protected Logger logger = LogManager.getLogger(ConsoleProxyGCThread.class);
private static final Logger logger = LogManager.getLogger(ConsoleProxyGCThread.class);
private final static int MAX_SESSION_IDLE_SECONDS = 180;
/**
* 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;
@ -46,21 +50,21 @@ public class ConsoleProxyGCThread extends Thread {
}
private void cleanupLogging() {
if (lastLogScan != 0 && System.currentTimeMillis() - lastLogScan < 3600000)
if (lastLogScan != 0 && System.currentTimeMillis() - lastLogScan < 3600000) {
return;
}
lastLogScan = System.currentTimeMillis();
File logDir = new File("./logs");
File files[] = logDir.listFiles();
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());
logger.info("[ignored] failed to delete file: " + e.getLocalizedMessage());
}
}
}
@ -78,10 +82,10 @@ public class ConsoleProxyGCThread extends Thread {
bReportLoad = false;
if (logger.isDebugEnabled()) {
logger.debug(String.format("connMap=%s, removedSessions=%s", connMap, removedSessionsSet));
logger.debug(String.format("ConsoleProxyGCThread loop: connMap=%s, removedSessions=%s", connMap, removedSessionsSet));
}
Set<String> e = connMap.keySet();
Iterator<String> iterator = e.iterator();
Set<String> keys = connMap.keySet();
Iterator<String> iterator = keys.iterator();
while (iterator.hasNext()) {
String key;
ConsoleProxyClient client;
@ -91,8 +95,12 @@ public class ConsoleProxyGCThread extends Thread {
client = connMap.get(key);
}
long seconds_unused = (System.currentTimeMillis() - client.getClientLastFrontEndActivityTime()) / 1000;
if (seconds_unused < MAX_SESSION_IDLE_SECONDS) {
if (client == null) {
continue;
}
long secondsUnused = (System.currentTimeMillis() - client.getClientLastFrontEndActivityTime()) / 1000;
if (secondsUnused < MAX_SESSION_IDLE_SECONDS) {
continue;
}
@ -102,12 +110,12 @@ public class ConsoleProxyGCThread extends Thread {
}
// close the server connection
logger.info("Dropping " + client + " which has not been used for " + seconds_unused + " seconds");
logger.info("Dropping " + client + " which has not been used for " + secondsUnused + " seconds");
client.closeClient();
}
if (bReportLoad || System.currentTimeMillis() - lastReportTick > 5000) {
// report load changes
// report load changes, including removed sessions since last report
ConsoleProxyClientStatsCollector collector = new ConsoleProxyClientStatsCollector(connMap);
collector.setRemovedSessions(new ArrayList<>(removedSessionsSet));
String loadInfo = collector.getStatsReport();
@ -125,7 +133,7 @@ public class ConsoleProxyGCThread extends Thread {
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
logger.debug("[ignored] Console proxy was interrupted during GC.");
logger.debug("[ignored] Console proxy GC thread interrupted.", ex);
}
}
}

View File

@ -40,8 +40,9 @@ 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;
protected Logger logger = LogManager.getLogger(getClass());
public ConsoleProxyNoVNCHandler() {
super();
@ -95,14 +96,14 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
String clientIp = session.getRemoteAddress().getAddress().getHostAddress();
boolean sessionRequiresNewViewer = Boolean.parseBoolean(queryMap.get("sessionRequiresNewViewer"));
if (tag == null)
if (tag == null) {
tag = "";
}
long ajaxSessionId = 0;
int port;
if (host == null || portStr == null || sid == null)
throw new IllegalArgumentException();
if (host == null || portStr == null || sid == null) {
throw new IllegalArgumentException("Missing required console connection parameters");
}
try {
port = Integer.parseInt(portStr);
@ -113,7 +114,7 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
if (ajaxSessionIdStr != null) {
try {
ajaxSessionId = Long.parseLong(ajaxSessionIdStr);
Long.parseLong(ajaxSessionIdStr);
} catch (NumberFormatException e) {
logger.error("Invalid ajaxSessionId (sess) value in query string: {}. Expected a number.", ajaxSessionIdStr, e);
throw new IllegalArgumentException(e);
@ -150,10 +151,11 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
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);
logger.error("Failed to create viewer [session UUID: {}, client IP: {}].", sessionUuid, clientIp, e);
return;
} finally {
if (viewer == null) {
@ -162,7 +164,7 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
}
}
private boolean checkSessionSourceIp(final Session session, final String sourceIP, String sessionSourceIP) throws IOException {
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);
@ -176,7 +178,7 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
@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);
logger.debug("Closing WebSocket session [source IP: {}, status code: {}, reason: {}].", sessionSourceIp, statusCode, reason);
if (viewer != null) {
ConsoleProxy.removeViewer(viewer);
}
@ -185,12 +187,20 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
@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) {
logger.error("Error on WebSocket [client ID: {}, session UUID: {}].", cause, viewer.getClientId(), viewer.getSessionUuid());
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);
}
}
}