Skip to content

Commit 222c572

Browse files
committed
feat: validate url
1 parent ae5014b commit 222c572

2 files changed

Lines changed: 172 additions & 7 deletions

File tree

base/src/main/java/com/tinyengine/it/mcp/tools/GitFileReaderService.java

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.tinyengine.it.common.utils.JsonUtils;
99
import com.tinyengine.it.mapper.ComponentLibraryMapper;
1010
import com.tinyengine.it.mapper.ComponentMapper;
11+
import com.tinyengine.it.mcp.utils.UrlValidateUtil;
1112
import com.tinyengine.it.model.dto.*;
1213
import com.tinyengine.it.model.entity.Component;
1314
import com.tinyengine.it.model.entity.ComponentLibrary;
@@ -36,10 +37,13 @@
3637
import java.time.LocalDateTime;
3738
import java.util.*;
3839

39-
40+
/**
41+
* GitFileReaderService
42+
*/
4043
@Service
4144
public class GitFileReaderService {
4245

46+
4347
private static final Logger log = LoggerFactory.getLogger(GitFileReaderService.class);
4448

4549
private static final int CONNECT_TIMEOUT = 30000;
@@ -48,11 +52,20 @@ public class GitFileReaderService {
4852
private ComponentLibraryMapper componentLibraryMapper;
4953
@Autowired
5054
private ComponentMapper baseMapper;
55+
@Autowired
56+
private UrlValidateUtil urlValidateUtil;
5157

58+
/**
59+
* Reads the content of a file from a given URL, validates its JSON structure,
60+
* and processes the data to create or update components and component libraries.
61+
*
62+
* @param url The URL of the file to be read.
63+
* @return A JSON string representing the result of the operation, or an error message if the process fails.
64+
*/
5265
@Tool(name="bundle_create",description = "给定的 文件URL 读取内容.")
5366
public String readFileFromRepo(String url) {
5467
try {
55-
68+
urlValidateUtil.validateFinalUrl(url);
5669
log.info("准备从 URL {} 中读取文件内容", url);
5770

5871
//1.从给定的 URL 读取内容
@@ -67,13 +80,12 @@ public String readFileFromRepo(String url) {
6780
}
6881
String fileName = Path.of(path).getFileName().toString();
6982
String jsonContent = new String(fileBytes, StandardCharsets.UTF_8);
70-
boolean validJson = isValidJson(jsonContent);
83+
String jsonString = removeBOM(jsonContent);
84+
boolean validJson = isValidJson(jsonString);
7185
if(!validJson) {
72-
log.error("从 URL {} 中读取的内容不是有效的 JSON: {}", url, jsonContent);
86+
log.error("从 URL {} 中读取的内容不是有效的 JSON: {}", url, jsonString);
7387
return "Error: 读取的内容不是有效的 JSON";
7488
}
75-
76-
String jsonString = removeBOM(jsonContent);
7789
Map<String, Object> jsonData =
7890
JsonUtils.MAPPER.readValue(jsonString, new TypeReference<Map<String, Object>>() {
7991
});
@@ -89,6 +101,9 @@ public String readFileFromRepo(String url) {
89101
data = (Map<String, Object>) dataObj;
90102
}
91103
BundleDto bundleDto = BeanUtil.mapToBean(data, BundleDto.class, true);
104+
if(bundleDto == null || bundleDto.getMaterials() == null) {
105+
throw new Exception("bundle.json 解析失败,缺少 materials 字段");
106+
}
92107
Result<BundleResultDto> bundleResultDtoResult = parseBundle(bundleDto);
93108
log.info("从 URL {} 中读取文件 '{}' 的内容并解析完成", url, path);
94109
log.info("组件列表: {}", bundleResultDtoResult.getData().getComponentList());
@@ -303,12 +318,14 @@ private List<Component> buildComponentList(BundleDto bundleDto, List<Map<String,
303318
}
304319

305320
public Result<BundleResultDto> parseBundle(BundleDto bundleDto) {
321+
306322
List<Map<String, Object>> components = bundleDto.getMaterials().getComponents();
307-
List<Child> snippets = bundleDto.getMaterials().getSnippets();
308323

309324
if (components == null || components.isEmpty()) {
310325
return Result.failed(ExceptionEnum.CM009);
311326
}
327+
List<Child> snippets = bundleDto.getMaterials().getSnippets();
328+
312329
List<Component> componentList = buildComponentList(bundleDto, components, snippets);
313330
List<Map<String, Object>> packages = bundleDto.getMaterials().getPackages();
314331

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package com.tinyengine.it.mcp.utils;
2+
3+
import com.tinyengine.it.common.exception.ServiceException;
4+
import com.tinyengine.it.config.OpenAIConfig;
5+
import org.springframework.stereotype.Component;
6+
7+
import java.net.*;
8+
import java.util.Arrays;
9+
import java.util.List;
10+
import java.util.Set;
11+
@Component
12+
public class UrlValidateUtil {
13+
private static final Set<String> LOOPBACK_HOSTS = Set.of("localhost", "127.0.0.1", "::1", "[::1]");
14+
private final OpenAIConfig config;
15+
16+
public UrlValidateUtil(OpenAIConfig config) {
17+
this.config = config;
18+
}
19+
20+
21+
public void validateFinalUrl(String finalUrl) {
22+
URI uri;
23+
try {
24+
uri = new URI(finalUrl);
25+
} catch (URISyntaxException e) {
26+
throw new ServiceException("400", "Invalid baseUrl format");
27+
}
28+
29+
String host = uri.getHost();
30+
if (host == null || host.isEmpty()) {
31+
throw new ServiceException("400", "Invalid baseUrl: missing host");
32+
}
33+
34+
boolean isLoopback = LOOPBACK_HOSTS.contains(host.toLowerCase());
35+
36+
List<String> allowedHosts = config.getAllowedHosts();
37+
38+
if (allowedHosts == null || allowedHosts.isEmpty()) {
39+
if (!config.isAllowAnyHost()) {
40+
throw new ServiceException("500", "No AI allowed hosts configured");
41+
}
42+
43+
enforceHttpsAndIpCheck(uri, host);
44+
return;
45+
}
46+
47+
boolean matched = allowedHosts.stream()
48+
.anyMatch(allowed -> allowed.equalsIgnoreCase(host));
49+
if (!matched) {
50+
throw new ServiceException("400",
51+
"Host not allowed: " + host + ". Allowed hosts: " + allowedHosts);
52+
}
53+
54+
if (isLoopback) {
55+
return;
56+
}
57+
58+
enforceHttpsAndIpCheck(uri, host);
59+
}
60+
61+
void enforceHttpsAndIpCheck(URI uri, String host) {
62+
String scheme = uri.getScheme();
63+
if (scheme == null || !"https".equalsIgnoreCase(scheme)) {
64+
throw new ServiceException("400", "Only HTTPS protocol is allowed for custom baseUrl");
65+
}
66+
67+
try {
68+
InetAddress[] addresses = resolveHostAddresses(host);
69+
boolean hasBlockedAddress = Arrays.stream(addresses).anyMatch(this::isBlockedAddress);
70+
if (hasBlockedAddress) {
71+
throw new ServiceException("400", "Internal network addresses are not allowed");
72+
}
73+
} catch (UnknownHostException e) {
74+
throw new ServiceException("400", "Unable to resolve host: " + host);
75+
}
76+
}
77+
78+
InetAddress[] resolveHostAddresses(String host) throws UnknownHostException {
79+
return InetAddress.getAllByName(host);
80+
}
81+
82+
boolean isBlockedAddress(InetAddress address) {
83+
if (address.isLoopbackAddress()
84+
|| address.isSiteLocalAddress()
85+
|| address.isLinkLocalAddress()
86+
|| address.isAnyLocalAddress()
87+
|| address.isMulticastAddress()) {
88+
return true;
89+
}
90+
91+
if (address instanceof Inet4Address) {
92+
return isBlockedIpv4((Inet4Address) address);
93+
}
94+
if (address instanceof Inet6Address) {
95+
return isBlockedIpv6((Inet6Address) address);
96+
}
97+
return false;
98+
}
99+
100+
private boolean isBlockedIpv4(Inet4Address address) {
101+
byte[] octets = address.getAddress();
102+
int first = octets[0] & 0xFF;
103+
int second = octets[1] & 0xFF;
104+
int third = octets[2] & 0xFF;
105+
106+
if (first == 0) {
107+
return true;
108+
}
109+
if (first == 100 && second >= 64 && second <= 127) {
110+
return true;
111+
}
112+
if (first == 192 && second == 0 && third == 0) {
113+
return true;
114+
}
115+
if (first == 192 && second == 0 && third == 2) {
116+
return true;
117+
}
118+
if (first == 198 && (second == 18 || second == 19)) {
119+
return true;
120+
}
121+
if (first == 198 && second == 51 && third == 100) {
122+
return true;
123+
}
124+
if (first == 203 && second == 0 && third == 113) {
125+
return true;
126+
}
127+
return first >= 240;
128+
}
129+
130+
private boolean isBlockedIpv6(Inet6Address address) {
131+
byte[] octets = address.getAddress();
132+
int first = octets[0] & 0xFF;
133+
int second = octets[1] & 0xFF;
134+
135+
if ((first & 0xFE) == 0xFC) {
136+
return true;
137+
}
138+
if (first == 0x20 && second == 0x01) {
139+
int third = octets[2] & 0xFF;
140+
int fourth = octets[3] & 0xFF;
141+
if (third == 0x0D && fourth == 0xB8) {
142+
return true;
143+
}
144+
}
145+
return first == 0xFF;
146+
}
147+
148+
}

0 commit comments

Comments
 (0)