Copilot commented on code in PR #12053:
URL: https://github.com/apache/cloudstack/pull/12053#discussion_r2523669136
##########
ui/src/utils/plugins.js:
##########
@@ -18,14 +18,36 @@
import _ from 'lodash'
import { i18n } from '@/locales'
import { getAPI } from '@/api'
-import { message, notification, Modal } from 'ant-design-vue'
+import { message, notification, Modal, Button } from 'ant-design-vue'
+import { h } from 'vue'
import eventBus from '@/config/eventBus'
import store from '@/store'
import { sourceToken } from '@/utils/request'
import { toLocalDate, toLocaleDate } from '@/utils/date'
export const pollJobPlugin = {
install (app) {
+ function canViewLogs (logIds) {
+ console.log('canViewLogs', store.getters.features.logswebserverenabled,
'createLogsWebSession' in store.getters.apis, logIds, logIds && logIds.length >
0)
Review Comment:
Console.log statement should be removed from production code. This debug
logging can expose sensitive information and impact performance.
##########
ui/src/utils/plugins.js:
##########
@@ -89,10 +113,19 @@ export const pollJobPlugin = {
this.$store.commit('SET_HEADER_NOTICES', jobs)
})
+ const allLogIds = []
+ if (logIds) {
+ allLogIds.push(...logIds)
+ }
+
options.originalPage = options.originalPage ||
this.$router.currentRoute.value.path
getAPI('queryAsyncJobResult', { jobId }).then(json => {
const result = json.queryasyncjobresultresponse
eventBus.emit('update-job-details', { jobId, resourceId })
+ if (result.logids) {
+ allLogIds.push(...result.logids)
+ }
+ console.log('pollJobPlugin', result.logids, allLogIds)
Review Comment:
Console.log statement should be removed from production code. Debug logging
should use proper logging mechanisms instead of console.log.
##########
plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionTokenCryptoUtil.java:
##########
@@ -0,0 +1,60 @@
+// 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 org.apache.cloudstack.logsws;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+
+import com.cloud.serializer.GsonHelper;
+
+public class LogsWebSessionTokenCryptoUtil {
+ private static final String ALGORITHM = "AES";
+ private static final String TRANSFORMATION = "AES";
Review Comment:
Weak encryption: Using AES in ECB mode (default when only "AES" is specified
as transformation) is insecure as it doesn't use an initialization vector.
Consider using "AES/GCM/NoPadding" or "AES/CBC/PKCS5Padding" with a random IV
for better security.
##########
engine/schema/src/main/resources/META-INF/db/schema-42200to42300.sql:
##########
@@ -18,3 +18,32 @@
--;
-- Schema upgrade from 4.22.0.0 to 4.23.0.0
--;
+
+-- Add management_server_details table to allow ManagementServer scope configs
+CREATE TABLE IF NOT EXISTS `cloud`.`management_server_details` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
+ `management_server_id` bigint unsigned NOT NULL COMMENT 'management server
the detail is related to',
+ `name` varchar(255) NOT NULL COMMENT 'name of the detail',
+ `value` varchar(255) NOT NULL,
+ `display` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'True if the detail can
be displayed to the end user',
+ PRIMARY KEY (`id`),
+ CONSTRAINT `fk_management_server_details__management_server_id` FOREIGN
KEY
`fk_management_server_details__management_server_id`(`management_server_id`)
REFERENCES `mshost`(`id`) ON DELETE CASCADE,
+ KEY `i_management_server_details__name__value` (`name`(128),`value`(128))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+-- Create table for Logs Web Session
+CREATE TABLE IF NOT EXISTS `cloud`.`logs_web_session` (
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the
session',
+ `uuid` varchar(40) NOT NULL COMMENT 'UUID generated for the session',
+ `filter` varchar(64) DEFAULT NULL COMMENT 'Filter keyword for the session',
Review Comment:
The field name in the database schema is 'filter' (singular) but the
converter is used for a List. This should be 'filters' (plural) to match the
Java field name and be semantically correct. The SQL definition on line 38
should use `filters` instead of `filter`.
```suggestion
`filters` varchar(64) DEFAULT NULL COMMENT 'Filter keywords for the
session',
```
##########
ui/src/components/view/SettingsTab.vue:
##########
@@ -85,6 +85,7 @@ export default {
}
},
created () {
+ console.log('---------------', this.$route.meta.name)
Review Comment:
Console.log statement should be removed from production code. This appears
to be debug code that was left in.
##########
ui/src/components/view/LogsConsole.vue:
##########
@@ -0,0 +1,515 @@
+// 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.
+
+<template>
+ <div>
+ <a-drawer
+ class="resizable-drawer"
+ :title="$t('Logs')"
+ placement="bottom"
+ :visible="visible"
+ :height="drawerHeight"
+ :maskClosable="false"
+ :bodyStyle="{ overflow: 'hidden' }"
+ @close="closeAction"
+ >
+ <template #extra>
+ <a-button type="primary" @click="onDownload" v-if="webSocketsValid">{{
$t('label.download') }}</a-button>
+ </template>
+ <!-- Draggable handle at the top of the drawer content -->
+ <div
+ class="drag-handle"
+ @mousedown="startDrag"
+ ></div>
+ <!-- Container that holds both the scrollable content and the fixed
footer -->
+ <div class="drawer-container">
+ <div class="header">
+ <a-row :gutter="[16, 8]" style="width: 100%; margin-bottom: 16px">
+ <a-col :xs="24" :sm="16">
+ <tooltip-label :title="$t('label.source')"
:tooltip="'Sources'" />
+ <a-select
+ v-model:value="selectedWebSockets"
+ mode="multiple"
+ showSearch
+ optionFilterProp="label"
+ :filterOption="(input, option) => {
+ return
option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
+ }"
+ :loading="webSocketsLoading"
+ :placeholder="'Sources'"
+ @change="handleSourceChange"
+ style="width: 100%">
+ <a-select-option
+ v-for="opt in webSockets"
+ :key="opt.id"
+ :label="opt.name">
+ {{ opt.name }}
+ </a-select-option>
+ </a-select>
+ </a-col>
+
+ <a-col :xs="24" :sm="8">
+ <tooltip-label :title="'Log Type'" :tooltip="'Log severity
level'" />
+ <a-select
+ mode="multiple"
+ v-model:value="selectedLogType"
+ :placeholder="'Log Type'"
+ style="width: 100%">
+ <a-select-option
+ v-for="level in logLevels"
+ :key="level"
+ :value="level">
+ {{ level }}
+ </a-select-option>
+ </a-select>
+ </a-col>
+ </a-row>
+ </div>
+ <div class="content_wrapper">
+ <div
+ v-if="showRawLogs"
+ class="content"
+ v-html="webSocketData">
Review Comment:
Potential XSS vulnerability: HTML content is rendered using `v-html` without
sanitization. The `webSocketData` contains unsanitized log data that could
include malicious scripts. Consider using a library like DOMPurify to sanitize
the HTML before rendering, or avoid using v-html altogether.
##########
framework/websocket-server/src/main/java/org/apache/cloudstack/framework/websocket/server/WebSocketServer.java:
##########
@@ -0,0 +1,207 @@
+// 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 org.apache.cloudstack.framework.websocket.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.KeyManagerFactory;
+
+import org.apache.cloudstack.framework.websocket.server.common.WebSocketRouter;
+import org.apache.cloudstack.utils.server.ServerPropertiesUtil;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslProvider;
+
+/**
+ * Netty WebSocket server that delegates routing to WebSocketRouter.
+ * Replaces the previous helper-based WebSocketServer.
+ */
+public final class WebSocketServer {
+ private static final Logger LOG =
LogManager.getLogger(WebSocketServer.class);
+
+ private final String host;
+ private final int port;
+ private final WebSocketRouter router;
+ private final String websocketBasePath;
+ private final boolean sslEnabled;
+
+ private EventLoopGroup bossGroup;
+ private EventLoopGroup workerGroup;
+ private Channel serverChannel;
+ private volatile boolean running;
+
+ public WebSocketServer(int port, WebSocketRouter router, boolean
sslEnabled) {
+ this(null, port, router, null, sslEnabled);
+ }
+
+ public WebSocketServer(String host, int port, WebSocketRouter router,
String websocketBasePath, boolean sslEnabled) {
+ this.host = StringUtils.isBlank(host) ? "0.0.0.0" : host;
+ this.port = port;
+ this.router = router;
+ this.websocketBasePath = StringUtils.isBlank(websocketBasePath) ?
+ WebSocketRouter.WEBSOCKET_PATH_PREFIX : websocketBasePath;
+ this.sslEnabled = sslEnabled;
+ }
+
+ protected KeyManagerFactory buildKeyManagerFactory(Path storePath, char[]
password) throws
+ KeyStoreException, IOException, NoSuchAlgorithmException,
CertificateException, UnrecoverableKeyException {
+ KeyStore ks = KeyStore.getInstance(detectType(storePath));
+ try (InputStream in = Files.newInputStream(storePath)) {
+ ks.load(in, password);
+ }
+ KeyManagerFactory kmf =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(ks, password);
+ return kmf;
+ }
+
+ private static String detectType(Path p) {
+ String name = p.getFileName().toString().toLowerCase();
+ return (name.endsWith(".p12") || name.endsWith(".pfx")) ? "PKCS12" :
"JKS";
+ }
+
+ /**
+ * Creates a Netty SslContext:
+ * uses only a keystore containing the server's private key and
certificate chain.
+ *
+ * @param keystoreFile Path to the keystore file (JKS or PKCS12)
+ * @param keystorePassword Password for both the keystore and key entry
+ * @return configured Netty SslContext
+ */
+ protected SslContext createServerSslContext(String keystoreFile, String
keystorePassword) throws
+ UnrecoverableKeyException, CertificateException,
KeyStoreException, IOException, NoSuchAlgorithmException {
+ KeyManagerFactory kmf = buildKeyManagerFactory(Path.of(keystoreFile),
keystorePassword.toCharArray());
+
+ // Build Netty SSL context (server mode) – same as Jetty's
SSLContextFactory.Server
+ return SslContextBuilder.forServer(kmf)
+ .sslProvider(SslProvider.JDK) // Use JDK
provider (same as Jetty)
+ .protocols("TLSv1.3", "TLSv1.2") // Match
Jetty default supported protocols
+ .build();
+ }
+
+ protected SslContext createServerSslContextIfNeeded() throws
IllegalArgumentException {
+ if (!sslEnabled) {
+ return null;
+ }
+ String keystoreFile =
ServerPropertiesUtil.getProperty(ServerPropertiesUtil.KEY_KEYSTORE_FILE);
+ String keystorePassword =
ServerPropertiesUtil.getProperty(ServerPropertiesUtil.KEY_KEYSTORE_FILE);
+ if (StringUtils.isBlank(keystoreFile) ||
StringUtils.isBlank(keystorePassword)) {
+ throw new IllegalArgumentException("SSL is enabled but keystore
file or password is not configured");
+ }
+ if (Files.exists(Path.of(keystoreFile))) {
Review Comment:
Inverted logic: the condition checks `Files.exists(...)` but the error
message says "does not exist". This should be
`!Files.exists(Path.of(keystoreFile))` to correctly validate that the keystore
file exists.
```suggestion
if (!Files.exists(Path.of(keystoreFile))) {
```
##########
framework/websocket-server/src/main/java/org/apache/cloudstack/framework/websocket/server/WebSocketServer.java:
##########
@@ -0,0 +1,207 @@
+// 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 org.apache.cloudstack.framework.websocket.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.KeyManagerFactory;
+
+import org.apache.cloudstack.framework.websocket.server.common.WebSocketRouter;
+import org.apache.cloudstack.utils.server.ServerPropertiesUtil;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslProvider;
+
+/**
+ * Netty WebSocket server that delegates routing to WebSocketRouter.
+ * Replaces the previous helper-based WebSocketServer.
+ */
+public final class WebSocketServer {
+ private static final Logger LOG =
LogManager.getLogger(WebSocketServer.class);
+
+ private final String host;
+ private final int port;
+ private final WebSocketRouter router;
+ private final String websocketBasePath;
+ private final boolean sslEnabled;
+
+ private EventLoopGroup bossGroup;
+ private EventLoopGroup workerGroup;
+ private Channel serverChannel;
+ private volatile boolean running;
+
+ public WebSocketServer(int port, WebSocketRouter router, boolean
sslEnabled) {
+ this(null, port, router, null, sslEnabled);
+ }
+
+ public WebSocketServer(String host, int port, WebSocketRouter router,
String websocketBasePath, boolean sslEnabled) {
+ this.host = StringUtils.isBlank(host) ? "0.0.0.0" : host;
+ this.port = port;
+ this.router = router;
+ this.websocketBasePath = StringUtils.isBlank(websocketBasePath) ?
+ WebSocketRouter.WEBSOCKET_PATH_PREFIX : websocketBasePath;
+ this.sslEnabled = sslEnabled;
+ }
+
+ protected KeyManagerFactory buildKeyManagerFactory(Path storePath, char[]
password) throws
+ KeyStoreException, IOException, NoSuchAlgorithmException,
CertificateException, UnrecoverableKeyException {
+ KeyStore ks = KeyStore.getInstance(detectType(storePath));
+ try (InputStream in = Files.newInputStream(storePath)) {
+ ks.load(in, password);
+ }
+ KeyManagerFactory kmf =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(ks, password);
+ return kmf;
+ }
+
+ private static String detectType(Path p) {
+ String name = p.getFileName().toString().toLowerCase();
+ return (name.endsWith(".p12") || name.endsWith(".pfx")) ? "PKCS12" :
"JKS";
+ }
+
+ /**
+ * Creates a Netty SslContext:
+ * uses only a keystore containing the server's private key and
certificate chain.
+ *
+ * @param keystoreFile Path to the keystore file (JKS or PKCS12)
+ * @param keystorePassword Password for both the keystore and key entry
+ * @return configured Netty SslContext
+ */
+ protected SslContext createServerSslContext(String keystoreFile, String
keystorePassword) throws
+ UnrecoverableKeyException, CertificateException,
KeyStoreException, IOException, NoSuchAlgorithmException {
+ KeyManagerFactory kmf = buildKeyManagerFactory(Path.of(keystoreFile),
keystorePassword.toCharArray());
+
+ // Build Netty SSL context (server mode) – same as Jetty's
SSLContextFactory.Server
+ return SslContextBuilder.forServer(kmf)
+ .sslProvider(SslProvider.JDK) // Use JDK
provider (same as Jetty)
+ .protocols("TLSv1.3", "TLSv1.2") // Match
Jetty default supported protocols
+ .build();
+ }
+
+ protected SslContext createServerSslContextIfNeeded() throws
IllegalArgumentException {
+ if (!sslEnabled) {
+ return null;
+ }
+ String keystoreFile =
ServerPropertiesUtil.getProperty(ServerPropertiesUtil.KEY_KEYSTORE_FILE);
+ String keystorePassword =
ServerPropertiesUtil.getProperty(ServerPropertiesUtil.KEY_KEYSTORE_FILE);
Review Comment:
Wrong property key is being used. Line 127 should retrieve
`KEY_KEYSTORE_PASSWORD` but it's retrieving `KEY_KEYSTORE_FILE` again. This
will cause SSL context creation to fail as it will use the keystore file path
as the password.
```suggestion
String keystorePassword =
ServerPropertiesUtil.getProperty(ServerPropertiesUtil.KEY_KEYSTORE_PASSWORD);
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]