更新 pom组件版本 新增安全秘钥接口 新增接口关闭 新增encryption接入方法 新增basic接入方法 新增User-Agent接入方法 支持ftp多用户 支持basic多用户 修复file接入协议错误 修复ssl伪证书错误 美化RUL报错提醒

This commit is contained in:
高雄
2025-12-25 17:04:18 +08:00
parent 92ca92bee6
commit 2dd008532a
14 changed files with 1774 additions and 769 deletions

View File

@@ -179,7 +179,7 @@ public class FileController {
return ReturnResponse.failure("文件名为空,删除失败!");
}
try {
fileName = WebUtils.decodeUrl(fileName);
fileName = WebUtils.decodeUrl(fileName,"base64");
} catch (Exception ex) {
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, fileName);
return ReturnResponse.failure(errorMsg + "删除失败!");
@@ -208,7 +208,7 @@ public class FileController {
public Object directory(String urls) {
String fileUrl;
try {
fileUrl = WebUtils.decodeUrl(urls);
fileUrl = WebUtils.decodeUrl(urls,"base64");
} catch (Exception ex) {
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "url");
return ReturnResponse.failure(errorMsg);

View File

@@ -1,18 +1,24 @@
package cn.keking.web.controller;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.service.FilePreviewFactory;
import cn.keking.service.cache.CacheService;
import cn.keking.service.impl.OtherFilePreviewImpl;
import cn.keking.utils.FtpUtils;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.SslUtils;
import cn.keking.utils.WebUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import fr.opensagres.xdocreport.core.io.IOUtils;
import org.apache.commons.codec.binary.Base64;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -24,6 +30,7 @@ import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.RestTemplate;
@@ -38,6 +45,8 @@ import java.util.List;
import java.util.Map;
import static cn.keking.service.FilePreview.PICTURE_FILE_PREVIEW_PAGE;
import static cn.keking.utils.KkFileUtils.isFtpUrl;
import static cn.keking.utils.KkFileUtils.isHttpUrl;
/**
* @author yudian-it
@@ -45,8 +54,14 @@ import static cn.keking.service.FilePreview.PICTURE_FILE_PREVIEW_PAGE;
@Controller
public class OnlinePreviewController {
public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!";
private final Logger logger = LoggerFactory.getLogger(OnlinePreviewController.class);
public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!";
private static final String ILLEGAL_ACCESS_MSG = "访问不合法:访问密码不正确";
private static final String INTERFACE_CLOSED_MSG = "接口关闭,禁止访问!";
private static final String URL_PARAM_FTP_USERNAME = "ftp.username";
private static final String URL_PARAM_FTP_PASSWORD = "ftp.password";
private static final String URL_PARAM_FTP_CONTROL_ENCODING = "ftp.control.encoding";
private static final String URL_PARAM_FTP_PORT = "ftp.control.port";
private final FilePreviewFactory previewFactory;
private final CacheService cacheService;
@@ -64,11 +79,18 @@ public class OnlinePreviewController {
}
@GetMapping( "/onlinePreview")
public String onlinePreview(String url, Model model, HttpServletRequest req) {
public String onlinePreview(@RequestParam String url,
@RequestParam(required = false) String key,
@RequestParam(required = false) String encryption,
Model model,
HttpServletRequest req) {
// 验证访问权限
if (WebUtils.validateKey(key)) {
return otherFilePreview.notSupportedFile(model, ILLEGAL_ACCESS_MSG);
}
String fileUrl;
try {
fileUrl = WebUtils.decodeUrl(url);
fileUrl = WebUtils.decodeUrl(url, encryption);
} catch (Exception ex) {
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "url");
return otherFilePreview.notSupportedFile(model, errorMsg);
@@ -85,10 +107,22 @@ public class OnlinePreviewController {
}
@GetMapping( "/picturesPreview")
public String picturesPreview(String urls, Model model, HttpServletRequest req) {
public String picturesPreview(@RequestParam String urls,
@RequestParam(required = false) String key,
@RequestParam(required = false) String encryption,
Model model,
HttpServletRequest req) {
// 1. 验证接口是否开启
if (!ConfigConstants.getPicturesPreview()) {
return otherFilePreview.notSupportedFile(model, INTERFACE_CLOSED_MSG);
}
//2. 验证访问权限
if (WebUtils.validateKey(key)) {
return otherFilePreview.notSupportedFile(model, ILLEGAL_ACCESS_MSG);
}
String fileUrls;
try {
fileUrls = WebUtils.decodeUrl(urls);
fileUrls = WebUtils.decodeUrl(urls, encryption);
// 防止XSS攻击
fileUrls = KkFileUtils.htmlEscape(fileUrls);
} catch (Exception ex) {
@@ -103,7 +137,7 @@ public class OnlinePreviewController {
String currentUrl = req.getParameter("currentUrl");
if (StringUtils.hasText(currentUrl)) {
String decodedCurrentUrl = new String(Base64.decodeBase64(currentUrl));
decodedCurrentUrl = KkFileUtils.htmlEscape(decodedCurrentUrl); // 防止XSS攻击
decodedCurrentUrl = KkFileUtils.htmlEscape(decodedCurrentUrl); // 防止XSS攻击
model.addAttribute("currentUrl", decodedCurrentUrl);
} else {
model.addAttribute("currentUrl", imgUrls.get(0));
@@ -119,35 +153,50 @@ public class OnlinePreviewController {
* @param response response
*/
@GetMapping("/getCorsFile")
public void getCorsFile(String urlPath, HttpServletResponse response,FileAttribute fileAttribute) throws IOException {
public void getCorsFile(@RequestParam String urlPath,
@RequestParam(required = false) String key,
HttpServletResponse response,
FileAttribute fileAttribute) throws Exception {
// 1. 验证接口是否开启
if (!ConfigConstants.getGetCorsFile()) {
logger.info("接口关闭,禁止访问!url{}", urlPath);
return;
}
//2. 验证访问权限
if (WebUtils.validateKey(key)) {
logger.info("访问不合法:访问密码不正确!url{}", urlPath);
return;
}
URL url;
try {
urlPath = WebUtils.decodeUrl(urlPath);
urlPath = WebUtils.decodeUrl(urlPath, "base64");
url = WebUtils.normalizedURL(urlPath);
} catch (Exception ex) {
logger.error(String.format(BASE64_DECODE_ERROR_MSG, urlPath),ex);
return;
}
assert urlPath != null;
if (!urlPath.toLowerCase().startsWith("http") && !urlPath.toLowerCase().startsWith("https") && !urlPath.toLowerCase().startsWith("ftp")) {
if (!isHttpUrl(url) && !isFtpUrl(url)) {
logger.info("读取跨域文件异常可能存在非法访问urlPath{}", urlPath);
return;
}
InputStream inputStream = null;
logger.info("读取跨域pdf文件url{}", urlPath);
if (!urlPath.toLowerCase().startsWith("ftp:")) {
factory.setConnectionRequestTimeout(2000);
factory.setConnectTimeout(10000);
factory.setReadTimeout(72000);
HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new DefaultRedirectStrategy()).build();
if (!isFtpUrl(url)) {
CloseableHttpClient httpClient = SslUtils.createHttpClientIgnoreSsl();
factory.setHttpClient(httpClient);
// restTemplate.setRequestFactory(factory);
restTemplate.setRequestFactory(factory);
RequestCallback requestCallback = request -> {
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
String proxyAuthorization = fileAttribute.getKkProxyAuthorization();
if(StringUtils.hasText(proxyAuthorization)){
Map<String,String> proxyAuthorizationMap = mapper.readValue(proxyAuthorization, Map.class);
proxyAuthorizationMap.forEach((key, value) -> request.getHeaders().set(key, value));
Map<String, String> proxyAuthorizationMap = mapper.readValue(
proxyAuthorization,
TypeFactory.defaultInstance().constructMapType(Map.class, String.class, String.class)
);
proxyAuthorizationMap.forEach((headerKey, value) -> request.getHeaders().set(headerKey, value));
}
};
try {
@@ -160,10 +209,16 @@ public class OnlinePreviewController {
}
}else{
try {
if(urlPath.contains(".svg")) {
response.setContentType("image/svg+xml");
String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1);
String contentType = WebUtils.getContentTypeByFilename(filename);
if (contentType != null) {
response.setContentType(contentType);
}
inputStream = (url).openStream();
String ftpUsername = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_USERNAME);
String ftpPassword = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_PASSWORD);
String ftpControlEncoding = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_CONTROL_ENCODING);
String support = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_PORT);
inputStream= FtpUtils.preview(urlPath,support, urlPath, ftpUsername, ftpPassword, ftpControlEncoding);
IOUtils.copy(inputStream, response.getOutputStream());
} catch (IOException e) {
logger.error("读取跨域文件异常url{}", urlPath);
@@ -180,9 +235,32 @@ public class OnlinePreviewController {
*/
@GetMapping("/addTask")
@ResponseBody
public String addQueueTask(String url) {
logger.info("添加转码队列url{}", url);
cacheService.addQueueTask(url);
public String addQueueTask(@RequestParam String url,
@RequestParam(required = false) String key,
@RequestParam(required = false) String encryption) {
// 1. 验证接口是否开启
if (!ConfigConstants.getAddTask()) {
String errorMsg = "接口关闭,禁止访问!";
logger.info("{}url{}", errorMsg, url);
return errorMsg;
}
String fileUrls;
try {
fileUrls = WebUtils.decodeUrl(url, encryption);
} catch (Exception ex) {
String errorMsg = "Url解析错误";
logger.info("{}url{}", errorMsg, url);
return errorMsg;
}
//2. 验证访问权限
if (WebUtils.validateKey(key)) {
String errorMsg = "访问不合法:访问密码不正确!";
logger.info("{}url{}", errorMsg, fileUrls);
return errorMsg;
}
logger.info("添加转码队列url{}", fileUrls);
cacheService.addQueueTask(fileUrls);
return "success";
}
}

View File

@@ -1,6 +1,8 @@
package cn.keking.web.filter;
import cn.keking.config.ConfigConstants;
import cn.keking.model.ReturnResponse;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils;
import io.mola.galimatias.GalimatiasParseException;
import org.jodconverter.core.util.OSUtils;
@@ -11,6 +13,8 @@ import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import jakarta.servlet.*;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLDecoder;
@@ -56,18 +60,42 @@ public class TrustDirFilter implements Filter {
}
private boolean allowPreview(String urlPath) {
//判断URL是否合法
if(!StringUtils.hasText(urlPath) || !WebUtils.isValidUrl(urlPath)) {
return false ;
// 判断URL是否合法
if (KkFileUtils.isIllegalFileName(urlPath) || !StringUtils.hasText(urlPath) || !WebUtils.isValidUrl(urlPath)) {
return false;
}
try {
URL url = WebUtils.normalizedURL(urlPath);
if ("file".equals(url.getProtocol().toLowerCase(Locale.ROOT))) {
String filePath = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8.name());
if (OSUtils.IS_OS_WINDOWS) {
filePath = filePath.replaceAll("/", "\\\\");
// 将文件路径转换为File对象
File targetFile = new File(filePath);
// 将配置目录也转换为File对象
File fileDir = new File(ConfigConstants.getFileDir());
File localPreviewDir = new File(ConfigConstants.getLocalPreviewDir());
try {
// 获取规范路径(系统会自动处理大小写、符号链接、相对路径等)
String canonicalFilePath = targetFile.getCanonicalPath();
String canonicalFileDir = fileDir.getCanonicalPath();
String canonicalLocalPreviewDir = localPreviewDir.getCanonicalPath();
// 检查文件是否在配置目录下
return isSubDirectory(canonicalFileDir, canonicalFilePath) || isSubDirectory(canonicalLocalPreviewDir, canonicalFilePath);
} catch (IOException e) {
logger.warn("获取规范路径失败,使用原始路径比较", e);
// 如果获取规范路径失败,回退到原始路径比较
String absFilePath = targetFile.getAbsolutePath();
String absFileDir = fileDir.getAbsolutePath();
String absLocalPreviewDir = localPreviewDir.getAbsolutePath();
// 统一路径分隔符
absFilePath = absFilePath.replace('\\', '/');
absFileDir = absFileDir.replace('\\', '/');
absLocalPreviewDir = absLocalPreviewDir.replace('\\', '/');
// 确保目录以斜杠结尾
if (!absFileDir.endsWith("/")) absFileDir += "/";
if (!absLocalPreviewDir.endsWith("/")) absLocalPreviewDir += "/";
return absFilePath.startsWith(absFileDir) || absFilePath.startsWith(absLocalPreviewDir);
}
return filePath.startsWith(ConfigConstants.getFileDir()) || filePath.startsWith(ConfigConstants.getLocalPreviewDir());
}
return true;
} catch (IOException | GalimatiasParseException e) {
@@ -75,4 +103,26 @@ public class TrustDirFilter implements Filter {
return false;
}
}
/**
* 检查子路径是否在父路径下(跨平台)
*/
private boolean isSubDirectory(String parentDir, String childPath) {
try {
File parent = new File(parentDir);
File child = new File(childPath);
// 获取规范路径
String canonicalParent = parent.getCanonicalPath();
String canonicalChild = child.getCanonicalPath();
// 确保父目录以路径分隔符结尾
if (!canonicalParent.endsWith(File.separator)) {
canonicalParent += File.separator;
}
// 比较路径
return canonicalChild.startsWith(canonicalParent);
} catch (IOException e) {
logger.warn("检查子路径失败", e);
return false;
}
}
}

View File

@@ -42,9 +42,9 @@ public class TrustHostFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String url = WebUtils.getSourceUrl(request);
String host = WebUtils.getHost(url);
String host = WebUtils.urlSecurity(url); //启用地址检查
assert host != null;
if (isNotTrustHost(host)) {
if (isNotTrustHost(host)||!WebUtils.isValidUrl(url)) {
String html = this.notTrustHostHtmlView.replace("${current_host}", host);
response.getWriter().write(html);
response.getWriter().close();
@@ -58,7 +58,6 @@ public class TrustHostFilter implements Filter {
if (CollectionUtils.isNotEmpty(ConfigConstants.getNotTrustHostSet())) {
return ConfigConstants.getNotTrustHostSet().contains(host);
}
// 如果配置了白名单,检查是否在白名单中
if (CollectionUtils.isNotEmpty(ConfigConstants.getTrustHostSet())) {
// 支持通配符 * 表示允许所有主机