Skip to content

Commit 29cac29

Browse files
authored
Merge pull request #1661 from lowcoder-org/nodeserver_encrypted_payload
Nodeserver encrypted payload
2 parents 10066c1 + bda2f16 commit 29cac29

File tree

10 files changed

+229
-24
lines changed

10 files changed

+229
-24
lines changed

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ public interface EncryptionService {
44

55
String encryptString(String plaintext);
66

7+
String encryptStringForNodeServer(String plaintext);
8+
79
String decryptString(String encryptedText);
810

911
String encryptPassword(String plaintext);

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionServiceImpl.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.lowcoder.sdk.config.CommonConfig;
66
import org.lowcoder.sdk.config.CommonConfig.Encrypt;
77
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.beans.factory.annotation.Value;
89
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
910
import org.springframework.security.crypto.encrypt.Encryptors;
1011
import org.springframework.security.crypto.encrypt.TextEncryptor;
@@ -14,13 +15,18 @@
1415
public class EncryptionServiceImpl implements EncryptionService {
1516

1617
private final TextEncryptor textEncryptor;
18+
private final TextEncryptor textEncryptorForNodeServer;
1719
private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
1820

1921
@Autowired
20-
public EncryptionServiceImpl(CommonConfig commonConfig) {
22+
public EncryptionServiceImpl(
23+
CommonConfig commonConfig
24+
) {
2125
Encrypt encrypt = commonConfig.getEncrypt();
2226
String saltInHex = Hex.encodeHexString(encrypt.getSalt().getBytes());
2327
this.textEncryptor = Encryptors.text(encrypt.getPassword(), saltInHex);
28+
String saltInHexForNodeServer = Hex.encodeHexString(commonConfig.getJsExecutor().getSalt().getBytes());
29+
this.textEncryptorForNodeServer = Encryptors.text(commonConfig.getJsExecutor().getPassword(), saltInHexForNodeServer);
2430
}
2531

2632
@Override
@@ -30,6 +36,13 @@ public String encryptString(String plaintext) {
3036
}
3137
return textEncryptor.encrypt(plaintext);
3238
}
39+
@Override
40+
public String encryptStringForNodeServer(String plaintext) {
41+
if (StringUtils.isEmpty(plaintext)) {
42+
return plaintext;
43+
}
44+
return textEncryptorForNodeServer.encrypt(plaintext);
45+
}
3346

3447
@Override
3548
public String decryptString(String encryptedText) {

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/plugin/client/DatasourcePluginClient.java

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import org.apache.commons.collections4.CollectionUtils;
66
import org.apache.commons.collections4.MapUtils;
77
import org.apache.commons.lang3.StringUtils;
8+
import org.lowcoder.domain.encryption.EncryptionService;
89
import org.lowcoder.domain.plugin.client.dto.DatasourcePluginDefinition;
910
import org.lowcoder.domain.plugin.client.dto.GetPluginDynamicConfigRequestDTO;
1011
import org.lowcoder.infra.js.NodeServerClient;
1112
import org.lowcoder.infra.js.NodeServerHelper;
13+
import org.lowcoder.sdk.config.CommonConfig;
1214
import org.lowcoder.sdk.config.CommonConfigHelper;
1315
import org.lowcoder.sdk.exception.ServerException;
1416
import org.lowcoder.sdk.models.DatasourceTestResult;
@@ -30,6 +32,8 @@
3032

3133
import static org.lowcoder.sdk.constants.GlobalContext.REQUEST;
3234

35+
import com.fasterxml.jackson.databind.ObjectMapper;
36+
3337
@Slf4j
3438
@RequiredArgsConstructor
3539
@Component
@@ -45,13 +49,17 @@ public class DatasourcePluginClient implements NodeServerClient {
4549
.build();
4650

4751
private final CommonConfigHelper commonConfigHelper;
52+
private final CommonConfig commonConfig;
4853
private final NodeServerHelper nodeServerHelper;
54+
private final EncryptionService encryptionService;
4955

5056
private static final String PLUGINS_PATH = "plugins";
5157
private static final String RUN_PLUGIN_QUERY = "runPluginQuery";
5258
private static final String VALIDATE_PLUGIN_DATA_SOURCE_CONFIG = "validatePluginDataSourceConfig";
5359
private static final String GET_PLUGIN_DYNAMIC_CONFIG = "getPluginDynamicConfig";
5460

61+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
62+
5563
public Mono<List<Object>> getPluginDynamicConfigSafely(List<GetPluginDynamicConfigRequestDTO> getPluginDynamicConfigRequestDTOS) {
5664
return getPluginDynamicConfig(getPluginDynamicConfigRequestDTOS)
5765
.onErrorResume(throwable -> {
@@ -119,21 +127,47 @@ public Flux<DatasourcePluginDefinition> getDatasourcePluginDefinitions() {
119127
@SuppressWarnings("unchecked")
120128
public Mono<QueryExecutionResult> executeQuery(String pluginName, Object queryDsl, List<Map<String, Object>> context, Object datasourceConfig) {
121129
return getAcceptLanguage()
122-
.flatMap(language -> WEB_CLIENT
123-
.post()
124-
.uri(nodeServerHelper.createUri(RUN_PLUGIN_QUERY))
125-
.header(HttpHeaders.ACCEPT_LANGUAGE, language)
126-
.bodyValue(Map.of("pluginName", pluginName, "dsl", queryDsl, "context", context, "dataSourceConfig", datasourceConfig))
127-
.exchangeToMono(response -> {
128-
if (response.statusCode().is2xxSuccessful()) {
129-
return response.bodyToMono(Map.class)
130-
.map(map -> map.get("result"))
131-
.map(QueryExecutionResult::success);
132-
}
133-
return response.bodyToMono(Map.class)
134-
.map(map -> MapUtils.getString(map, "message"))
135-
.map(QueryExecutionResult::errorWithMessage);
136-
}));
130+
.flatMap(language -> {
131+
try {
132+
Map<String, Object> body = Map.of(
133+
"pluginName", pluginName,
134+
"dsl", queryDsl,
135+
"context", context,
136+
"dataSourceConfig", datasourceConfig
137+
);
138+
String json = OBJECT_MAPPER.writeValueAsString(body);
139+
140+
boolean encryptionEnabled = !(commonConfig.getJsExecutor().getPassword().isEmpty() || commonConfig.getJsExecutor().getSalt().isEmpty());
141+
String payload;
142+
WebClient.RequestBodySpec requestSpec = WEB_CLIENT
143+
.post()
144+
.uri(nodeServerHelper.createUri(RUN_PLUGIN_QUERY))
145+
.header(HttpHeaders.ACCEPT_LANGUAGE, language);
146+
147+
if (encryptionEnabled) {
148+
payload = encryptionService.encryptStringForNodeServer(json);
149+
requestSpec = requestSpec.header("X-Encrypted", "true");
150+
} else {
151+
payload = json;
152+
}
153+
154+
return requestSpec
155+
.bodyValue(payload)
156+
.exchangeToMono(response -> {
157+
if (response.statusCode().is2xxSuccessful()) {
158+
return response.bodyToMono(Map.class)
159+
.map(map -> map.get("result"))
160+
.map(QueryExecutionResult::success);
161+
}
162+
return response.bodyToMono(Map.class)
163+
.map(map -> MapUtils.getString(map, "message"))
164+
.map(QueryExecutionResult::errorWithMessage);
165+
});
166+
} catch (Exception e) {
167+
log.error("Encryption error", e);
168+
return Mono.error(new ServerException("Encryption error"));
169+
}
170+
});
137171
}
138172

139173
@SuppressWarnings("unchecked")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package org.lowcoder.domain.encryption;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
import org.lowcoder.sdk.config.CommonConfig;
6+
import org.lowcoder.sdk.config.CommonConfig.Encrypt;
7+
import org.lowcoder.sdk.config.CommonConfig.JsExecutor;
8+
import org.springframework.security.crypto.encrypt.Encryptors;
9+
import org.springframework.security.crypto.encrypt.TextEncryptor;
10+
11+
import static org.junit.jupiter.api.Assertions.*;
12+
import static org.mockito.Mockito.*;
13+
14+
class EncryptionServiceImplTest {
15+
16+
private EncryptionServiceImpl encryptionService;
17+
private TextEncryptor nodeServerEncryptor;
18+
private String nodePassword = "nodePassword";
19+
private String nodeSalt = "nodeSalt";
20+
21+
@BeforeEach
22+
void setUp() {
23+
// Mock CommonConfig and its nested classes
24+
Encrypt encrypt = mock(Encrypt.class);
25+
when(encrypt.getPassword()).thenReturn("testPassword");
26+
when(encrypt.getSalt()).thenReturn("testSalt");
27+
28+
JsExecutor jsExecutor = mock(JsExecutor.class);
29+
when(jsExecutor.getPassword()).thenReturn(nodePassword);
30+
when(jsExecutor.getSalt()).thenReturn(nodeSalt);
31+
32+
CommonConfig commonConfig = mock(CommonConfig.class);
33+
when(commonConfig.getEncrypt()).thenReturn(encrypt);
34+
when(commonConfig.getJsExecutor()).thenReturn(jsExecutor);
35+
36+
encryptionService = new EncryptionServiceImpl(commonConfig);
37+
38+
// For direct comparison in test
39+
String saltInHexForNodeServer = org.apache.commons.codec.binary.Hex.encodeHexString(nodeSalt.getBytes());
40+
nodeServerEncryptor = Encryptors.text(nodePassword, saltInHexForNodeServer);
41+
}
42+
43+
@Test
44+
void testEncryptStringForNodeServer_NullInput() {
45+
assertNull(encryptionService.encryptStringForNodeServer(null));
46+
}
47+
48+
@Test
49+
void testEncryptStringForNodeServer_EmptyInput() {
50+
assertEquals("", encryptionService.encryptStringForNodeServer(""));
51+
}
52+
53+
@Test
54+
void testEncryptStringForNodeServer_EncryptsAndDecryptsCorrectly() {
55+
String plain = "node secret";
56+
String encrypted = encryptionService.encryptStringForNodeServer(plain);
57+
assertNotNull(encrypted);
58+
assertNotEquals(plain, encrypted);
59+
60+
// Decrypt using the same encryptor to verify correctness
61+
String decrypted = nodeServerEncryptor.decrypt(encrypted);
62+
assertEquals(plain, decrypted);
63+
}
64+
65+
@Test
66+
void testEncryptStringForNodeServer_DifferentInputsProduceDifferentOutputs() {
67+
String encrypted1 = encryptionService.encryptStringForNodeServer("abc");
68+
String encrypted2 = encryptionService.encryptStringForNodeServer("def");
69+
assertNotEquals(encrypted1, encrypted2);
70+
}
71+
72+
@Test
73+
void testEncryptStringForNodeServer_SameInputProducesDifferentOutputs() {
74+
String input = "repeat";
75+
String encrypted1 = encryptionService.encryptStringForNodeServer(input);
76+
String encrypted2 = encryptionService.encryptStringForNodeServer(input);
77+
// Spring's Encryptors.text uses random IV, so outputs should differ
78+
assertNotEquals(encrypted1, encrypted2);
79+
}
80+
}

server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ public long getMaxAgeInSeconds() {
147147
@Data
148148
public static class JsExecutor {
149149
private String host;
150+
private String password;
151+
private String salt;
152+
private boolean isEncrypted;
150153
}
151154

152155
@Data

server/api-service/lowcoder-server/src/main/resources/application-debug.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ common:
3737
cookie-name: LOWCODER_DEBUG_TOKEN
3838
js-executor:
3939
host: "http://127.0.0.1:6060"
40+
password: ${LOWCODER_NODE_SERVICE_SECRET:}
41+
salt: ${LOWCODER_NODE_SERVICE_SECRET_SALT:}
4042
workspace:
4143
mode: ${LOWCODER_WORKSPACE_MODE:SAAS}
4244
plugin-dirs:

server/api-service/lowcoder-server/src/main/resources/application.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ common:
7474
corsAllowedDomainString: ${LOWCODER_CORS_DOMAINS:*}
7575
js-executor:
7676
host: ${LOWCODER_NODE_SERVICE_URL:http://127.0.0.1:6060}
77+
password: ${LOWCODER_NODE_SERVICE_SECRET:}
78+
salt: ${LOWCODER_NODE_SERVICE_SECRET_SALT:}
7779
max-query-request-size: ${LOWCODER_MAX_REQUEST_SIZE:20m}
7880
max-query-response-size: ${LOWCODER_MAX_REQUEST_SIZE:20m}
7981
max-upload-size: ${LOWCODER_MAX_REQUEST_SIZE:20m}
@@ -129,4 +131,4 @@ management:
129131
redis:
130132
enabled: true
131133
diskspace:
132-
enabled: false
134+
enabled: false

server/node-service/src/controllers/plugins.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@ import { Request, Response } from "express";
33
import _ from "lodash";
44
import { Config } from "lowcoder-sdk/dataSource";
55
import * as pluginServices from "../services/plugin";
6+
// Add import for decryption utility
7+
import { decryptString } from "../utils/encryption"; // <-- implement this utility as needed
8+
9+
async function getDecryptedBody(req: Request): Promise<any> {
10+
if (req.headers["x-encrypted"]) {
11+
// Assume body is a raw encrypted string, decrypt and parse as JSON
12+
const encrypted = typeof req.body === "string" ? req.body : req.body?.toString?.();
13+
if (!encrypted) throw badRequest("Missing encrypted body");
14+
const decrypted = await decryptString(encrypted);
15+
try {
16+
return JSON.parse(decrypted);
17+
} catch (e) {
18+
throw badRequest("Failed to parse decrypted body as JSON");
19+
}
20+
}
21+
return req.body;
22+
}
623

724
export async function listPlugins(req: Request, res: Response) {
825
let ids = req.query["id"] || [];
@@ -15,12 +32,10 @@ export async function listPlugins(req: Request, res: Response) {
1532
}
1633

1734
export async function runPluginQuery(req: Request, res: Response) {
18-
const { pluginName, dsl, context, dataSourceConfig } = req.body;
35+
const body = await getDecryptedBody(req);
36+
const { pluginName, dsl, context, dataSourceConfig } = body;
1937
const ctx = pluginServices.getPluginContext(req);
2038

21-
22-
// console.log("pluginName: ", pluginName, "dsl: ", dsl, "context: ", context, "dataSourceConfig: ", dataSourceConfig, "ctx: ", ctx);
23-
2439
const result = await pluginServices.runPluginQuery(
2540
pluginName,
2641
dsl,
@@ -32,7 +47,8 @@ export async function runPluginQuery(req: Request, res: Response) {
3247
}
3348

3449
export async function validatePluginDataSourceConfig(req: Request, res: Response) {
35-
const { pluginName, dataSourceConfig } = req.body;
50+
const body = await getDecryptedBody(req);
51+
const { pluginName, dataSourceConfig } = body;
3652
const ctx = pluginServices.getPluginContext(req);
3753
const result = await pluginServices.validatePluginDataSourceConfig(
3854
pluginName,
@@ -50,10 +66,11 @@ type GetDynamicDefReqBody = {
5066

5167
export async function getDynamicDef(req: Request, res: Response) {
5268
const ctx = pluginServices.getPluginContext(req);
53-
if (!Array.isArray(req.body)) {
69+
const body = await getDecryptedBody(req);
70+
if (!Array.isArray(body)) {
5471
throw badRequest("request body is not a valid array");
5572
}
56-
const fields = req.body as GetDynamicDefReqBody;
73+
const fields = body as GetDynamicDefReqBody;
5774
const result: Config[] = [];
5875
for (const item of fields) {
5976
const def = await pluginServices.getDynamicConfigDef(

server/node-service/src/server.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { collectDefaultMetrics } from "prom-client";
99
import apiRouter from "./routes/apiRouter";
1010
import systemRouter from "./routes/systemRouter";
1111
import cors, { CorsOptions } from "cors";
12+
import bodyParser from "body-parser";
1213
collectDefaultMetrics();
1314

1415
const prefix = "/node-service";
@@ -32,6 +33,15 @@ router.use(morgan("dev"));
3233
/** Parse the request */
3334
router.use(express.urlencoded({ extended: false }));
3435

36+
/** Custom middleware: use raw body for encrypted requests */
37+
router.use((req, res, next) => {
38+
if (req.headers["x-encrypted"]) {
39+
bodyParser.text({ type: "*/*" })(req, res, next);
40+
} else {
41+
bodyParser.json()(req, res, next);
42+
}
43+
});
44+
3545
/** Takes care of JSON data */
3646
router.use(
3747
express.json({

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy