Skip to content

Commit 467d43f

Browse files
committed
fix(设备管理): 限制物模型导入使用托管文件
Closes: #766
1 parent 481e7f6 commit 467d43f

4 files changed

Lines changed: 87 additions & 24 deletions

File tree

jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/excel/DefaultImportExportService.java

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,13 @@
2020
import org.jetlinks.community.io.excel.easyexcel.ExcelReadDataListener;
2121
import org.jetlinks.community.io.file.FileManager;
2222
import org.jetlinks.community.io.utils.FileUtils;
23-
import org.springframework.core.io.Resource;
24-
import org.springframework.core.io.buffer.DataBuffer;
2523
import org.springframework.core.io.buffer.DataBufferUtils;
26-
import org.springframework.http.MediaType;
2724
import org.springframework.stereotype.Component;
28-
import org.springframework.web.reactive.function.client.WebClient;
2925
import reactor.core.publisher.Flux;
3026
import reactor.core.publisher.Mono;
3127

32-
import java.io.FileInputStream;
3328
import java.io.InputStream;
29+
import java.net.URI;
3430

3531
import static org.hswebframework.reactor.excel.ReactorExcel.read;
3632

@@ -41,13 +37,9 @@
4137
@Component
4238
public class DefaultImportExportService implements ImportExportService {
4339

44-
private WebClient client;
45-
4640
private final FileManager fileManager;
4741

48-
public DefaultImportExportService(WebClient.Builder builder,
49-
FileManager fileManager) {
50-
client = builder.build();
42+
public DefaultImportExportService(FileManager fileManager) {
5143
this.fileManager = fileManager;
5244
}
5345

@@ -80,19 +72,29 @@ public <T> Flux<T> readData(String fileUrl, String fileId, RowWrapper<T> wrapper
8072
}
8173

8274
public Mono<InputStream> getInputStream(String fileUrl) {
75+
// 导入入口只接受平台托管文件 ID,避免由请求参数触发任意网络或本地文件访问。
76+
return fileManager
77+
.read(resolveFileId(fileUrl))
78+
.as(DataBufferUtils::join)
79+
.map(buffer -> buffer.asInputStream(true));
80+
}
8381

84-
return Mono.defer(() -> {
85-
if (fileUrl.startsWith("http")) {
86-
return client
87-
.get()
88-
.uri(fileUrl)
89-
.accept(MediaType.APPLICATION_OCTET_STREAM)
90-
.exchangeToMono(clientResponse -> clientResponse.bodyToMono(Resource.class))
91-
.flatMap(resource -> Mono.fromCallable(resource::getInputStream));
92-
} else {
93-
return Mono.fromCallable(() -> new FileInputStream(fileUrl));
82+
static String resolveFileId(String fileUrl) {
83+
URI uri = URI.create(fileUrl);
84+
if (!uri.isAbsolute()) {
85+
if (fileUrl.contains("/") || fileUrl.contains("\\")) {
86+
throw new IllegalArgumentException("Only managed file IDs are supported");
9487
}
95-
});
88+
return fileUrl;
89+
}
9690

91+
String path = uri.getPath();
92+
int filePathIndex = path == null ? -1 : path.lastIndexOf("/file/");
93+
if (filePathIndex < 0) {
94+
throw new IllegalArgumentException("Only managed file URLs are supported");
95+
}
96+
String fileName = path.substring(filePathIndex + "/file/".length());
97+
int extensionIndex = fileName.indexOf('.');
98+
return extensionIndex > 0 ? fileName.substring(0, extensionIndex) : fileName;
9799
}
98100
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2026 JetLinks https://www.jetlinks.cn
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.jetlinks.community.io.excel;
17+
18+
import org.jetlinks.community.io.file.FileManager;
19+
import org.junit.jupiter.api.Test;
20+
import reactor.core.publisher.Flux;
21+
import reactor.test.StepVerifier;
22+
23+
import static org.junit.jupiter.api.Assertions.assertEquals;
24+
import static org.junit.jupiter.api.Assertions.assertThrows;
25+
import static org.mockito.Mockito.mock;
26+
import static org.mockito.Mockito.verify;
27+
import static org.mockito.Mockito.when;
28+
29+
class DefaultImportExportServiceTest {
30+
31+
@Test
32+
void shouldReadManagedFileInsteadOfRemoteUrl() {
33+
FileManager fileManager = mock(FileManager.class);
34+
when(fileManager.read("file-id")).thenReturn(Flux.empty());
35+
36+
DefaultImportExportService service = new DefaultImportExportService(fileManager);
37+
38+
StepVerifier
39+
.create(service.getInputStream("http://localhost:8848/api/file/file-id.csv?accessKey=test"))
40+
.verifyComplete();
41+
42+
verify(fileManager).read("file-id");
43+
}
44+
45+
@Test
46+
void shouldResolveManagedFileId() {
47+
assertEquals("file-id", DefaultImportExportService.resolveFileId("file-id"));
48+
assertEquals(
49+
"file-id",
50+
DefaultImportExportService.resolveFileId("http://localhost:8848/api/file/file-id.xlsx?accessKey=test")
51+
);
52+
assertThrows(
53+
IllegalArgumentException.class,
54+
() -> DefaultImportExportService.resolveFileId("http://127.0.0.1/internal/secret.csv")
55+
);
56+
assertThrows(
57+
IllegalArgumentException.class,
58+
() -> DefaultImportExportService.resolveFileId("/etc/passwd")
59+
);
60+
}
61+
}

jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1078,7 +1078,7 @@ public Mono<ValidationResult> deviceIdValidate2(@RequestParam @Parameter(descrip
10781078
@SaveAction
10791079
@Operation(summary = "解析文件为属性物模型")
10801080
public Mono<String> importPropertyMetadata(@PathVariable @Parameter(description = "产品ID") String productId,
1081-
@RequestParam @Parameter(description = "文件地址,支持csv,xlsx文件格式") String fileUrl) {
1081+
@RequestParam @Parameter(description = "平台文件ID,支持csv,xlsx文件格式") String fileUrl) {
10821082
return metadataManager
10831083
.getMetadataExpandsConfig(productId, DeviceMetadataType.property, "*", "*", DeviceConfigScope.device)
10841084
.collectList()

jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceProductController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ public Mono<Void> downloadExportPropertyMetadataTemplate(@PathVariable @Paramete
336336
@SaveAction
337337
@Operation(summary = "解析文件为属性物模型")
338338
public Mono<String> importPropertyMetadata(@PathVariable @Parameter(description = "产品ID") String productId,
339-
@RequestParam @Parameter(description = "文件地址,支持csv,xlsx文件格式") String fileUrl) {
339+
@RequestParam @Parameter(description = "平台文件ID,支持csv,xlsx文件格式") String fileUrl) {
340340
return configMetadataManager
341341
.getMetadataExpandsConfig(productId, DeviceMetadataType.property, "*", "*", DeviceConfigScope.product)
342342
.collectList()
@@ -355,4 +355,4 @@ public Mono<String> importPropertyMetadata(@PathVariable @Parameter(description
355355
});
356356
}
357357

358-
}
358+
}

0 commit comments

Comments
 (0)