Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
- `application.yml` 和 `application-druid.yml` 中配置数据库与上传路径
- 配置 Redis 地址与密码
- `endless-ui/vue.config.js` 中配置代理 API 地址
- 启动后可访问 http://localhost:8080/setup.html 在页面中编辑外置配置(默认仅本机访问,可设置 `setup.allow-remote=true` 放开远程)

4. 启动服务:

Expand Down Expand Up @@ -459,4 +460,3 @@
---

💡 **打造更专业的 Minecraft 运维体验,从这里开始!**

12 changes: 12 additions & 0 deletions endless-admin/src/main/java/cc/endmc/EndlessApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* 启动程序
Expand All @@ -23,6 +25,8 @@
@EnableScheduling
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}, scanBasePackages = "cc.endmc")
public class EndlessApplication {
private static final Logger log = LoggerFactory.getLogger(EndlessApplication.class);

public static void main(String[] args) {
// 初始化配置文件
try {
Expand All @@ -40,6 +44,14 @@ public static void main(String[] args) {

// 获取版本信息
String version = context.getEnvironment().getProperty("endless.version", "Unknown");
String serverPort = context.getEnvironment().getProperty("server.port", "8080");
String contextPath = context.getEnvironment().getProperty("server.servlet.context-path", "");
if (contextPath.isEmpty() || "/".equals(contextPath))
{
contextPath = "";
}
String setupUrl = String.format("http://localhost:%s%s/setup.html", serverPort, contextPath);
log.info("🔧 配置向导地址: {} (仅本机访问,可设置 setup.allow-remote=true)", setupUrl);

// 收集初始化数据
int serverCount = context.getBean(IServerInfoService.class).selectServerInfoList(new ServerInfo()).size();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package cc.endmc.web.controller.setup;

import cc.endmc.common.annotation.Anonymous;
import cc.endmc.common.core.domain.AjaxResult;
import cc.endmc.common.utils.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.error.YAMLException;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.Set;

/**
* 配置向导接口
*/
@Anonymous
@RestController
@RequestMapping("/setup")
public class SetupConfigController
{
private static final Logger log = LoggerFactory.getLogger(SetupConfigController.class);
private static final Path CONFIG_DIR = Paths.get("config");
private static final Set<String> ALLOWED_FILES = Set.of("application.yml", "application-druid.yml");
private static final Yaml SAFE_YAML = new Yaml(new SafeConstructor(new LoaderOptions()));

@Value("${setup.allow-remote:false}")
private boolean allowRemote;

@GetMapping("/config")
public AjaxResult getConfig(@RequestParam(value = "file", defaultValue = "application.yml") String file,
HttpServletRequest request)
{
if (!isRequestAllowed(request))
{
return AjaxResult.error("仅允许本机访问配置向导,如需远程访问请设置 setup.allow-remote=true");
}
Path configPath = resolveConfigPath(file);
if (configPath == null)
{
return AjaxResult.error("不支持的配置文件: " + file);
}
if (!Files.exists(configPath))
{
return AjaxResult.error("配置文件不存在: " + file);
}
try
{
String content = Files.readString(configPath, StandardCharsets.UTF_8);
HashMap<String, Object> data = new HashMap<>();
data.put("file", file);
data.put("content", content);
return AjaxResult.success(data);
}
catch (IOException e)
{
log.error("读取配置文件失败: {}", configPath.toAbsolutePath(), e);
return AjaxResult.error("读取配置文件失败");
}
}

@PostMapping(value = "/config", consumes = MediaType.TEXT_PLAIN_VALUE)
public AjaxResult saveConfig(@RequestParam(value = "file", defaultValue = "application.yml") String file,
@RequestBody(required = false) String content,
HttpServletRequest request)
{
if (!isRequestAllowed(request))
{
return AjaxResult.error("仅允许本机访问配置向导,如需远程访问请设置 setup.allow-remote=true");
}
Path configPath = resolveConfigPath(file);
if (configPath == null)
{
return AjaxResult.error("不支持的配置文件: " + file);
}
if (StringUtils.isBlank(content))
{
return AjaxResult.error("配置内容不能为空");
}
if (!isValidYaml(content))
{
return AjaxResult.error("配置内容不是有效的YAML格式");
}
try
{
Files.createDirectories(CONFIG_DIR);
Files.writeString(configPath, content, StandardCharsets.UTF_8, StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
log.info("配置文件已保存: {}", configPath.toAbsolutePath());
return AjaxResult.success("保存成功,请重启服务使配置生效");
}
catch (IOException e)
{
log.error("保存配置文件失败: {}", configPath.toAbsolutePath(), e);
return AjaxResult.error("保存配置文件失败");
}
}

private Path resolveConfigPath(String file)
{
if (!ALLOWED_FILES.contains(file))
{
return null;
}
Path configDir = CONFIG_DIR.toAbsolutePath().normalize();
Path normalized = configDir.resolve(file).normalize();
if (!normalized.startsWith(configDir))
{
return null;
}
return normalized;
}

private boolean isRequestAllowed(HttpServletRequest request)
{
return allowRemote || isLocalAddress(request.getRemoteAddr());
}
Comment on lines +133 to +136
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isRequestAllowed 仅基于 request.getRemoteAddr() 的 loopback 判断。在反向代理(Nginx/Traefik)部署且代理与应用同机时,外网请求的 remoteAddr 可能始终是 127.0.0.1/::1,从而绕过“仅本机访问”限制。另外当前全局 CORS 允许任意 Origin 时,浏览器可跨站向 localhost 发起 POST,形成 CSRF 风险。建议在 allow-remote=false 时:检测并拒绝带 Forwarded/X-Forwarded-* 的请求,且为保存操作增加一次性 token/密码校验或显式限制 Origin/关闭 /setup/** 的 CORS。

Copilot uses AI. Check for mistakes.

private boolean isLocalAddress(String address)
{
if (address == null)
{
return false;
}
try
{
return InetAddress.getByName(address).isLoopbackAddress();
}
catch (UnknownHostException ex)
{
return false;
}
}

private boolean isValidYaml(String content)
{
try
{
SAFE_YAML.load(content);
return true;
}
catch (YAMLException ex)
{
log.warn("YAML格式校验失败", ex);
return false;
}
Comment on lines +154 to +165
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

YAML 校验这里使用 SAFE_YAML.load(content):它只解析第一个 document(--- 之后的内容可能未被校验),并且 LoaderOptions 未设置 codePointLimit / maxAliasesForCollections 等限制,远程放开时可能被构造超大/高别名输入导致解析耗时或内存膨胀。建议改为遍历 loadAll 校验所有 documents,并在 LoaderOptions 上配置合理上限。

Copilot uses AI. Check for mistakes.
}
}
5 changes: 5 additions & 0 deletions endless-admin/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ server:
# Tomcat启动初始化的线程数,默认值10
min-spare: 10

# 配置向导
setup:
# 是否允许远程访问配置向导(默认仅本机访问)
allow-remote: false

# 日志配置
logging:
level:
Expand Down
Loading
Loading