Skip to content

Commit d4b9d57

Browse files
committed
fix
Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
1 parent a65a115 commit d4b9d57

File tree

15 files changed

+164
-85
lines changed

15 files changed

+164
-85
lines changed

client/src/main/java/org/apache/cloudstack/websocket/JettyWebSocketServlet.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.logging.log4j.LogManager;
3131
import org.apache.logging.log4j.Logger;
3232
import org.eclipse.jetty.websocket.api.Session;
33+
import org.eclipse.jetty.websocket.api.UpgradeRequest;
3334
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
3435
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
3536
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
@@ -151,16 +152,23 @@ private Map<String, String> parse(String q) {
151152
}
152153

153154
@Override
154-
public void onWebSocketConnect(Session jettySess) {
155-
super.onWebSocketConnect(jettySess);
156-
this.session = JettyWebSocketSession.adapt(jettySess, routePath, parse(rawQuery));
155+
public void onWebSocketConnect(Session jettySession) {
156+
super.onWebSocketConnect(jettySession);
157+
this.session = JettyWebSocketSession.adapt(jettySession, routePath, parse(rawQuery));
158+
UpgradeRequest request = jettySession.getUpgradeRequest();
159+
String remoteAddr = request.getHeader("X-Forwarded-For");
160+
if (remoteAddr == null) {
161+
remoteAddr = jettySession.getRemoteAddress().getAddress().getHostAddress();
162+
}
163+
this.session.setAttr(WebSocketSession.ATTR_REMOTE_ADDR, remoteAddr);
157164
try {
165+
158166
handler.onOpen(session);
159167
} catch (Throwable t) {
160168
try {
161169
handler.onError(session, t);
162170
} finally {
163-
jettySess.close();
171+
jettySession.close();
164172
}
165173
}
166174
}

engine/schema/src/main/java/org/apache/cloudstack/util/StringListJsonConverter.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,28 @@ public class StringListJsonConverter implements AttributeConverter<List<String>,
2929

3030
private static final ObjectMapper mapper = new ObjectMapper();
3131

32-
@Override
33-
public String convertToDatabaseColumn(List<String> attribute) {
32+
public static String getDatabaseColumnTypeValue(List<String> attribute) {
3433
try {
3534
return attribute == null ? null : mapper.writeValueAsString(attribute);
3635
} catch (JsonProcessingException e) {
3736
throw new IllegalArgumentException("Error converting list to JSON", e);
3837
}
3938
}
4039

40+
public static boolean isValidAttribute(List<String> attribute, int length) {
41+
try {
42+
String json = getDatabaseColumnTypeValue(attribute);
43+
return json != null && json.length() <= length;
44+
} catch (IllegalArgumentException e) {
45+
return false;
46+
}
47+
}
48+
49+
@Override
50+
public String convertToDatabaseColumn(List<String> attribute) {
51+
return getDatabaseColumnTypeValue(attribute);
52+
}
53+
4154
@Override
4255
public List<String> convertToEntityAttribute(String dbData) {
4356
try {

engine/schema/src/main/resources/META-INF/db/schema-42200to42300.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`management_server_details` (
3535
CREATE TABLE IF NOT EXISTS `cloud`.`logs_web_session` (
3636
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the session',
3737
`uuid` varchar(40) NOT NULL COMMENT 'UUID generated for the session',
38-
`filter` varchar(64) DEFAULT NULL COMMENT 'Filter keyword for the session',
38+
`filters` varchar(128) DEFAULT NULL COMMENT 'Filter keywords for the session',
3939
`created` datetime NOT NULL COMMENT 'When the session was created',
4040
`domain_id` bigint(20) unsigned NOT NULL COMMENT 'Domain of the account who generated the session',
4141
`account_id` bigint(20) unsigned NOT NULL COMMENT 'Account who generated the session',

framework/websocket-server/src/main/java/org/apache/cloudstack/framework/websocket/server/NettyWebSocketSession.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@
2424

2525
import org.apache.cloudstack.framework.websocket.server.common.WebSocketSession;
2626

27+
import io.netty.buffer.Unpooled;
2728
import io.netty.channel.Channel;
29+
import io.netty.channel.ChannelFutureListener;
2830
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
31+
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
2932
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
33+
import io.netty.util.AttributeKey;
3034

3135
final class NettyWebSocketSession implements WebSocketSession {
3236
private final Channel ch;
@@ -61,25 +65,25 @@ public void sendText(String text) {
6165

6266
@Override
6367
public void sendBinary(ByteBuffer buf) {
64-
io.netty.buffer.ByteBuf bb = io.netty.buffer.Unpooled.wrappedBuffer(buf);
68+
io.netty.buffer.ByteBuf bb = Unpooled.wrappedBuffer(buf);
6569
ch.writeAndFlush(new BinaryWebSocketFrame(bb));
6670
}
6771

6872
@Override
6973
public void close(int code, String reason) {
70-
ch.writeAndFlush(new io.netty.handler.codec.http.websocketx.CloseWebSocketFrame(code, reason))
71-
.addListener(io.netty.channel.ChannelFutureListener.CLOSE);
74+
ch.writeAndFlush(new CloseWebSocketFrame(code, reason))
75+
.addListener(ChannelFutureListener.CLOSE);
7276
}
7377

7478
@Override
7579
public <T> void setAttr(String key, T val) {
76-
ch.attr(io.netty.util.AttributeKey.valueOf(key)).set(val);
80+
ch.attr(AttributeKey.valueOf(key)).set(val);
7781
}
7882

7983
@SuppressWarnings("unchecked")
8084
@Override
8185
public <T> T getAttr(String key) {
82-
return (T) ch.attr(io.netty.util.AttributeKey.valueOf(key)).get();
86+
return (T) ch.attr(AttributeKey.valueOf(key)).get();
8387
}
8488

8589
Channel unwrap() {

framework/websocket-server/src/main/java/org/apache/cloudstack/framework/websocket/server/WebSocketServer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@ protected SslContext createServerSslContextIfNeeded() throws IllegalArgumentExce
124124
return null;
125125
}
126126
String keystoreFile = ServerPropertiesUtil.getProperty(ServerPropertiesUtil.KEY_KEYSTORE_FILE);
127-
String keystorePassword = ServerPropertiesUtil.getProperty(ServerPropertiesUtil.KEY_KEYSTORE_FILE);
127+
String keystorePassword = ServerPropertiesUtil.getProperty(ServerPropertiesUtil.KEY_KEYSTORE_PASSWORD);
128128
if (StringUtils.isBlank(keystoreFile) || StringUtils.isBlank(keystorePassword)) {
129129
throw new IllegalArgumentException("SSL is enabled but keystore file or password is not configured");
130130
}
131-
if (Files.exists(Path.of(keystoreFile))) {
131+
if (!Files.exists(Path.of(keystoreFile))) {
132132
throw new IllegalArgumentException(String.format("SSL is enabled but keystore file does not exist: %s",
133133
keystoreFile));
134134
}

framework/websocket-server/src/main/java/org/apache/cloudstack/framework/websocket/server/WebSocketServerRoutingHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
8080
}
8181

8282
session = new NettyWebSocketSession(ctx.channel(), path, QueryUtils.parse(rawQuery));
83-
session.setAttr("remoteAddress", String.valueOf(ctx.channel().remoteAddress()));
83+
session.setAttr(WebSocketSession.ATTR_REMOTE_ADDR, String.valueOf(ctx.channel().remoteAddress()));
8484

8585
try {
8686
handler.onOpen(session);

framework/websocket-server/src/main/java/org/apache/cloudstack/framework/websocket/server/common/WebSocketSession.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.util.Map;
2222

2323
public interface WebSocketSession {
24+
String ATTR_REMOTE_ADDR = "remoteAddress";
25+
2426
String id();
2527

2628
String path();

plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSession.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import org.apache.cloudstack.api.InternalIdentity;
2626

2727
public interface LogsWebSession extends ControlledEntity, Identity, InternalIdentity {
28+
int MAX_FILTERS_LENGTH = 128;
29+
2830
long getId();
2931
List<String> getFilters();
3032
long getDomainId();

plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionApiServiceImpl.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.apache.cloudstack.logsws.api.response.LogsWebSessionWebSocketResponse;
4343
import org.apache.cloudstack.logsws.dao.LogsWebSessionDao;
4444
import org.apache.cloudstack.logsws.vo.LogsWebSessionVO;
45+
import org.apache.cloudstack.util.StringListJsonConverter;
4546
import org.apache.cloudstack.utils.identity.ManagementServerNode;
4647
import org.apache.commons.collections.CollectionUtils;
4748
import org.apache.commons.collections.MapUtils;
@@ -120,12 +121,18 @@ public LogsWebSessionResponse createLogsWebSession(CreateLogsWebSessionCmd cmd)
120121
if (!accountService.isRootAdmin(caller.getAccountId())) {
121122
throw new PermissionDeniedException("Invalid request");
122123
}
123-
for (String filter : filters) {
124-
if (StringUtils.isBlank(filter)) {
125-
throw new InvalidParameterValueException(String.format("Invalid value for parameter - %s",
126-
ApiConstants.FILTERS));
124+
if (CollectionUtils.isNotEmpty(filters)) {
125+
for (String filter : filters) {
126+
if (StringUtils.isBlank(filter)) {
127+
throw new InvalidParameterValueException(String.format("Invalid value for parameter - %s",
128+
ApiConstants.FILTERS));
129+
}
130+
}
131+
if (!StringListJsonConverter.isValidAttribute(filters, LogsWebSession.MAX_FILTERS_LENGTH)) {
132+
throw new InvalidParameterValueException("Combined filters length too long");
127133
}
128134
}
135+
129136
if (!logsWSManager.canCreateNewLogsWebSession()) {
130137
throw new CloudRuntimeException("Failed to create Logs Web Session as max session limit reached");
131138
}

plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionTokenCryptoUtil.java

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,44 +17,90 @@
1717

1818
package org.apache.cloudstack.logsws;
1919

20+
import java.nio.ByteBuffer;
2021
import java.nio.charset.StandardCharsets;
2122
import java.security.GeneralSecurityException;
23+
import java.security.MessageDigest;
24+
import java.security.SecureRandom;
2225
import java.util.Base64;
2326

2427
import javax.crypto.Cipher;
28+
import javax.crypto.SecretKey;
29+
import javax.crypto.spec.GCMParameterSpec;
2530
import javax.crypto.spec.SecretKeySpec;
2631

2732
import com.cloud.serializer.GsonHelper;
2833

2934
public class LogsWebSessionTokenCryptoUtil {
3035
private static final String ALGORITHM = "AES";
31-
private static final String TRANSFORMATION = "AES";
32-
private static final String KEY_PREFIX = "Logger";
36+
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
3337

34-
private static String getDesiredLengthKey(String key) {
35-
if (key.length() >= 16) {
36-
return key.substring(0, 16);
37-
}
38-
return KEY_PREFIX.substring(0, 16 - key.length()) + key;
38+
private static final int IV_LENGTH_BYTES = 12;
39+
private static final int GCM_TAG_LENGTH_BITS = 128;
40+
private static final byte TOKEN_VERSION = 1;
41+
42+
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
43+
44+
private static SecretKey deriveKey(String keyMaterial) throws GeneralSecurityException {
45+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
46+
byte[] hash = digest.digest(keyMaterial.getBytes(StandardCharsets.UTF_8));
47+
return new SecretKeySpec(hash, 0, 32, ALGORITHM);
3948
}
4049

41-
public static String encrypt(LogsWebSessionTokenPayload payload, String key) throws GeneralSecurityException {
50+
public static String encrypt(LogsWebSessionTokenPayload payload, String keyMaterial)
51+
throws GeneralSecurityException {
52+
4253
String json = GsonHelper.getGson().toJson(payload);
43-
SecretKeySpec secretKey = new SecretKeySpec(getDesiredLengthKey(key).getBytes(StandardCharsets.UTF_8), ALGORITHM);
54+
byte[] plaintext = json.getBytes(StandardCharsets.UTF_8);
55+
56+
SecretKey key = deriveKey(keyMaterial);
57+
58+
byte[] iv = new byte[IV_LENGTH_BYTES];
59+
SECURE_RANDOM.nextBytes(iv);
60+
4461
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
45-
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
46-
byte[] encrypted = cipher.doFinal(json.getBytes(StandardCharsets.UTF_8));
47-
// URL-safe Base64 (no '/' or '+') and remove padding
48-
return Base64.getUrlEncoder().withoutPadding().encodeToString(encrypted);
62+
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv);
63+
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
64+
65+
byte[] ciphertextWithTag = cipher.doFinal(plaintext);
66+
ByteBuffer buffer = ByteBuffer.allocate(1 + IV_LENGTH_BYTES + ciphertextWithTag.length);
67+
buffer.put(TOKEN_VERSION);
68+
buffer.put(iv);
69+
buffer.put(ciphertextWithTag);
70+
71+
return Base64.getUrlEncoder().withoutPadding().encodeToString(buffer.array());
4972
}
5073

51-
public static LogsWebSessionTokenPayload decrypt(String token, String key) throws GeneralSecurityException {
52-
byte[] decoded = Base64.getUrlDecoder().decode(token);
53-
SecretKeySpec secretKey = new SecretKeySpec(getDesiredLengthKey(key).getBytes(StandardCharsets.UTF_8), ALGORITHM);
74+
public static LogsWebSessionTokenPayload decrypt(String token, String keyMaterial)
75+
throws GeneralSecurityException {
76+
77+
byte[] allBytes = Base64.getUrlDecoder().decode(token);
78+
ByteBuffer buffer = ByteBuffer.wrap(allBytes);
79+
80+
if (buffer.remaining() < 1 + IV_LENGTH_BYTES + 1) {
81+
throw new GeneralSecurityException("Invalid token format");
82+
}
83+
84+
byte version = buffer.get();
85+
if (version != TOKEN_VERSION) {
86+
throw new GeneralSecurityException("Unsupported token version: " + version);
87+
}
88+
89+
byte[] iv = new byte[IV_LENGTH_BYTES];
90+
buffer.get(iv);
91+
92+
byte[] ciphertextWithTag = new byte[buffer.remaining()];
93+
buffer.get(ciphertextWithTag);
94+
95+
SecretKey key = deriveKey(keyMaterial);
96+
5497
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
55-
cipher.init(Cipher.DECRYPT_MODE, secretKey);
56-
byte[] decrypted = cipher.doFinal(decoded);
57-
String json = new String(decrypted, StandardCharsets.UTF_8);
98+
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv);
99+
cipher.init(Cipher.DECRYPT_MODE, key, spec);
100+
101+
byte[] plaintext = cipher.doFinal(ciphertextWithTag);
102+
String json = new String(plaintext, StandardCharsets.UTF_8);
103+
58104
return GsonHelper.getGson().fromJson(json, LogsWebSessionTokenPayload.class);
59105
}
60106
}

0 commit comments

Comments
 (0)