-
Notifications
You must be signed in to change notification settings - Fork 35
Add local-only web setup editor for external config #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
pilgrimage233
merged 3 commits into
development
from
copilot/add-configuration-setup-web-page
Feb 7, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
endless-admin/src/main/java/cc/endmc/web/controller/setup/SetupConfigController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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()); | ||
| } | ||
|
|
||
| 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
|
||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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。