mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-04-13 20:11:32 +00:00
Merge PR342 into master with trust-host conflict resolution
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,32 +1,25 @@
|
||||
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.KkFileUtils;
|
||||
import cn.keking.utils.WebUtils;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import cn.keking.utils.*;
|
||||
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.HttpClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Controller;
|
||||
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;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -35,9 +28,10 @@ import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
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,16 +39,19 @@ 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;
|
||||
private final FileHandlerService fileHandlerService;
|
||||
private final OtherFilePreviewImpl otherFilePreview;
|
||||
private static final RestTemplate restTemplate = new RestTemplate();
|
||||
private static final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
public OnlinePreviewController(FilePreviewFactory filePreviewFactory, FileHandlerService fileHandlerService, CacheService cacheService, OtherFilePreviewImpl otherFilePreview) {
|
||||
this.previewFactory = filePreviewFactory;
|
||||
@@ -64,16 +61,31 @@ 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,
|
||||
@RequestParam(defaultValue = "false") String highlightall,
|
||||
@RequestParam(defaultValue = "0") String page,
|
||||
@RequestParam(defaultValue = "false") String kkagent,
|
||||
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);
|
||||
}
|
||||
FileAttribute fileAttribute = fileHandlerService.getFileAttribute(fileUrl, req); //这里不在进行URL 处理了
|
||||
FileAttribute fileAttribute = fileHandlerService.getFileAttribute(fileUrl, req);
|
||||
|
||||
highlightall= KkFileUtils.htmlEscape(highlightall);
|
||||
model.addAttribute("highlightall", highlightall);
|
||||
model.addAttribute("page", page);
|
||||
model.addAttribute("kkagent", kkagent);
|
||||
model.addAttribute("file", fileAttribute);
|
||||
FilePreview filePreview = previewFactory.get(fileAttribute);
|
||||
logger.info("预览文件url:{},previewType:{}", fileUrl, fileAttribute.getType());
|
||||
@@ -85,10 +97,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 +127,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,51 +143,54 @@ 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,
|
||||
HttpServletRequest req,
|
||||
@RequestParam(required = false) String encryption) 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, encryption);
|
||||
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;
|
||||
}
|
||||
FileAttribute fileAttribute = fileHandlerService.getFileAttribute(urlPath, req);
|
||||
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();
|
||||
factory.setHttpClient(httpClient);
|
||||
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));
|
||||
}
|
||||
};
|
||||
logger.info("读取跨域文件url:{}", urlPath);
|
||||
if (!isFtpUrl(url)) {
|
||||
CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient();
|
||||
|
||||
HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> IOUtils.copy(responseWrapper.getInputStream(), response.getOutputStream()));
|
||||
} else {
|
||||
try {
|
||||
restTemplate.execute(url.toURI(), HttpMethod.GET, requestCallback, fileResponse -> {
|
||||
IOUtils.copy(fileResponse.getBody(), response.getOutputStream());
|
||||
return null;
|
||||
});
|
||||
} catch (Exception e) {
|
||||
System.out.println(e);
|
||||
}
|
||||
}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 +207,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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,14 @@ public class AttributeSetFilter implements Filter {
|
||||
request.setAttribute("homePagination", ConfigConstants.getHomePagination());
|
||||
request.setAttribute("homePageSize", ConfigConstants.getHomePageSize());
|
||||
request.setAttribute("homeSearch", ConfigConstants.getHomeSearch());
|
||||
request.setAttribute("isshowaeskey", ConfigConstants.getisShowaesKey());
|
||||
request.setAttribute("isjavascript", ConfigConstants.getisJavaScript());
|
||||
request.setAttribute("xlsxallowEdit", ConfigConstants.getxlsxAllowEdit());
|
||||
request.setAttribute("xlsxshowtoolbar", ConfigConstants.getxlsxShowtoolbar());
|
||||
request.setAttribute("aeskey", ConfigConstants.getaesKey());
|
||||
request.setAttribute("isshowkey", ConfigConstants.getisShowKey());
|
||||
request.setAttribute("kkkey", ConfigConstants.getKey());
|
||||
request.setAttribute("scriptjs", ConfigConstants.getscriptJs());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class TrustHostFilter implements Filter {
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
String url = WebUtils.getSourceUrl(request);
|
||||
String host = WebUtils.getHost(url);
|
||||
if (isNotTrustHost(host)) {
|
||||
if (isNotTrustHost(host) || !WebUtils.isValidUrl(url)) {
|
||||
String currentHost = host == null ? "UNKNOWN" : host;
|
||||
if (response instanceof HttpServletResponse httpServletResponse) {
|
||||
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
@@ -78,7 +78,6 @@ public class TrustHostFilter implements Filter {
|
||||
&& matchAnyPattern(host, ConfigConstants.getNotTrustHostSet())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果配置了白名单,检查是否在白名单中
|
||||
if (CollectionUtils.isNotEmpty(ConfigConstants.getTrustHostSet())) {
|
||||
// 支持通配符 * 表示允许所有主机
|
||||
|
||||
Reference in New Issue
Block a user