mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-04-08 17:27:34 +00:00
更新 pom组件版本 新增安全秘钥接口 新增接口关闭 新增encryption接入方法 新增basic接入方法 新增User-Agent接入方法 支持ftp多用户 支持basic多用户 修复file接入协议错误 修复ssl伪证书错误 美化RUL报错提醒
This commit is contained in:
32
pom.xml
32
pom.xml
@@ -10,36 +10,36 @@
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<jodconverter.version>4.4.6</jodconverter.version>
|
||||
<jodconverter.version>4.4.11</jodconverter.version>
|
||||
<spring.boot.version>3.5.6</spring.boot.version>
|
||||
<poi.version>5.2.2</poi.version>
|
||||
<poi.version>5.2.5</poi.version>
|
||||
<xdocreport.version>1.0.6</xdocreport.version>
|
||||
<xstream.version>1.4.20</xstream.version>
|
||||
<xstream.version>1.4.21</xstream.version>
|
||||
<junrar.version>7.5.5</junrar.version>
|
||||
<redisson.version>3.22.0</redisson.version>
|
||||
<redisson.version>4.0.0</redisson.version>
|
||||
<sevenzipjbinding.version>16.02-2.01</sevenzipjbinding.version>
|
||||
<jchardet.version>1.0</jchardet.version>
|
||||
<antlr.version>2.7.7</antlr.version>
|
||||
<concurrentlinkedhashmap.version>1.4.2</concurrentlinkedhashmap.version>
|
||||
<rocksdb.version>5.17.2</rocksdb.version>
|
||||
<pdfbox.version>3.0.2</pdfbox.version>
|
||||
<pdfbox.version>3.0.6</pdfbox.version>
|
||||
<jai-imageio.version>1.4.0</jai-imageio.version>
|
||||
<jbig2-imageio.version>3.0.4</jbig2-imageio.version>
|
||||
<galimatias.version>0.2.1</galimatias.version>
|
||||
<bytedeco.version>1.5.2</bytedeco.version>
|
||||
<opencv.version>4.1.2-1.5.2</opencv.version>
|
||||
<openblas.version>0.3.6-1.5.1</openblas.version>
|
||||
<ffmpeg.version>4.2.1-1.5.2</ffmpeg.version>
|
||||
<itextpdf.version>5.5.13.3</itextpdf.version>
|
||||
<httpclient.version>3.1</httpclient.version>
|
||||
<aspose-cad.version>23.9</aspose-cad.version>
|
||||
<bytedeco.version>1.5.12</bytedeco.version>
|
||||
<opencv.version>4.11.0-1.5.12</opencv.version>
|
||||
<openblas.version>0.3.30-1.5.12</openblas.version>
|
||||
<ffmpeg.version>7.1.1-1.5.12</ffmpeg.version>
|
||||
<itextpdf.version>5.5.13.4</itextpdf.version>
|
||||
<httpclient.version>5.6</httpclient.version>
|
||||
<aspose-cad.version>25.10</aspose-cad.version>
|
||||
<bcprov-jdk15on.version>1.70</bcprov-jdk15on.version>
|
||||
<juniversalchardet.version>1.0.3</juniversalchardet.version>
|
||||
<httpcomponents.version>4.5.14</httpcomponents.version>
|
||||
<httpcomponents.version>4.5.16</httpcomponents.version>
|
||||
|
||||
<commons-cli.version>1.5.0</commons-cli.version>
|
||||
<commons-net.version>3.9.0</commons-net.version>
|
||||
<commons-lang3.version>3.13.0</commons-lang3.version>
|
||||
<commons-cli.version>1.11.0</commons-cli.version>
|
||||
<commons-net.version>3.12.0</commons-net.version>
|
||||
<commons-lang3.version>3.20.0</commons-lang3.version>
|
||||
|
||||
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
79
server/src/main/java/cn/keking/utils/AESUtil.java
Normal file
79
server/src/main/java/cn/keking/utils/AESUtil.java
Normal file
@@ -0,0 +1,79 @@
|
||||
package cn.keking.utils;
|
||||
|
||||
import cn.keking.config.ConfigConstants;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* AES加密解密工具类(目前AES比DES和DES3更安全,速度更快,对称加密一般采用AES)
|
||||
*/
|
||||
public class AESUtil {
|
||||
private static final String aesKey = ConfigConstants.getaesKey();
|
||||
|
||||
/**
|
||||
* AES解密
|
||||
*/
|
||||
public static String AesDecrypt(String url) {
|
||||
if (!aesKey(aesKey)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
byte[] raw = aesKey.getBytes(StandardCharsets.UTF_8);
|
||||
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
|
||||
byte[] encrypted1 = Base64.getDecoder().decode(url);//先用base64解密
|
||||
byte[] original = cipher.doFinal(encrypted1);
|
||||
return new String(original, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
if (e.getMessage().contains("Given final block not properly padded. Such issues can arise if a bad key is used during decryption")) {
|
||||
return "Keyerror";
|
||||
}else if (e.getMessage().contains("Input byte array has incorrect ending byte")) {
|
||||
return "byteerror";
|
||||
}else if (e.getMessage().contains("Illegal base64 character")) {
|
||||
return "base64error";
|
||||
}else if (e.getMessage().contains("Input length must be multiple of 16 when decrypting with padded cipher")) {
|
||||
return "byteerror";
|
||||
}else {
|
||||
System.out.println("ace错误:"+e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密
|
||||
*/
|
||||
public static String aesEncrypt(String url) {
|
||||
if (!aesKey(aesKey)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
byte[] raw = aesKey.getBytes(StandardCharsets.UTF_8);
|
||||
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");//"算法/模式/补码方式"
|
||||
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
|
||||
byte[] encrypted = cipher.doFinal(url.getBytes(StandardCharsets.UTF_8));
|
||||
return new String(Base64.getEncoder().encode(encrypted));//此处使用BASE64做转码功能,同时能起到2次加密的作用。
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean aesKey(String aesKey) {
|
||||
if (aesKey == null) {
|
||||
System.out.print("Key为空null");
|
||||
return false;
|
||||
}
|
||||
// 判断Key是否为16位
|
||||
if (aesKey.length() != 16) {
|
||||
System.out.print("Key长度不是16位");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,10 @@ import cn.keking.config.ConfigConstants;
|
||||
import cn.keking.model.FileAttribute;
|
||||
import cn.keking.model.ReturnResponse;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
import io.mola.galimatias.GalimatiasParseException;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
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;
|
||||
@@ -22,12 +21,13 @@ import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static cn.keking.utils.KkFileUtils.isFtpUrl;
|
||||
import static cn.keking.utils.KkFileUtils.isHttpUrl;
|
||||
import static cn.keking.utils.KkFileUtils.*;
|
||||
|
||||
/**
|
||||
* @author yudian-it
|
||||
@@ -39,6 +39,7 @@ public class DownloadUtils {
|
||||
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 static final RestTemplate restTemplate = new RestTemplate();
|
||||
private static final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
@@ -53,7 +54,6 @@ public class DownloadUtils {
|
||||
// 忽略ssl证书
|
||||
String urlStr = null;
|
||||
try {
|
||||
SslUtils.ignoreSsl();
|
||||
urlStr = fileAttribute.getUrl().replaceAll("\\+", "%20").replaceAll(" ", "%20");
|
||||
} catch (Exception e) {
|
||||
logger.error("忽略SSL证书异常:", e);
|
||||
@@ -90,17 +90,18 @@ public class DownloadUtils {
|
||||
if (!fileAttribute.getSkipDownLoad()) {
|
||||
if (isHttpUrl(url)) {
|
||||
File realFile = new File(realPath);
|
||||
factory.setConnectionRequestTimeout(2000); //设置超时时间
|
||||
factory.setConnectTimeout(10000);
|
||||
factory.setReadTimeout(72000);
|
||||
HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new DefaultRedirectStrategy()).build();
|
||||
factory.setHttpClient(httpClient); //加入重定向方法
|
||||
CloseableHttpClient httpClient = SslUtils.createHttpClientIgnoreSsl();
|
||||
factory.setHttpClient(httpClient);
|
||||
restTemplate.setRequestFactory(factory);
|
||||
RequestCallback requestCallback = request -> {
|
||||
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
|
||||
WebUtils.applyBasicAuthHeaders(request.getHeaders(), fileAttribute);
|
||||
String proxyAuthorization = fileAttribute.getKkProxyAuthorization();
|
||||
if(StringUtils.hasText(proxyAuthorization)){
|
||||
Map<String,String> proxyAuthorizationMap = mapper.readValue(proxyAuthorization, Map.class);
|
||||
Map<String, String> proxyAuthorizationMap = mapper.readValue(
|
||||
proxyAuthorization,
|
||||
TypeFactory.defaultInstance().constructMapType(Map.class, String.class, String.class)
|
||||
);
|
||||
proxyAuthorizationMap.forEach((key, value) -> request.getHeaders().set(key, value));
|
||||
}
|
||||
};
|
||||
@@ -110,16 +111,19 @@ public class DownloadUtils {
|
||||
return null;
|
||||
});
|
||||
} catch (Exception e) {
|
||||
response.setCode(1);
|
||||
response.setContent(null);
|
||||
response.setMsg("下载失败:" + e);
|
||||
return response;
|
||||
response.setCode(1);
|
||||
response.setContent(null);
|
||||
response.setMsg("下载失败:" + e);
|
||||
return response;
|
||||
}
|
||||
} else if (isFtpUrl(url)) {
|
||||
String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME);
|
||||
String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD);
|
||||
String ftpControlEncoding = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_CONTROL_ENCODING);
|
||||
FtpUtils.download(fileAttribute.getUrl(), realPath, ftpUsername, ftpPassword, ftpControlEncoding);
|
||||
String ftpport = WebUtils.getUrlParameterReg(realPath, URL_PARAM_FTP_PORT);
|
||||
FtpUtils.download(fileAttribute.getUrl(),ftpport, realPath, ftpUsername, ftpPassword, ftpControlEncoding);
|
||||
} else if (isFileUrl(url)) { // 添加对file协议的支持
|
||||
handleFileProtocol(url, realPath);
|
||||
} else {
|
||||
response.setCode(1);
|
||||
response.setMsg("url不能识别url" + urlStr);
|
||||
@@ -138,10 +142,70 @@ public class DownloadUtils {
|
||||
response.setMsg(e.getMessage());
|
||||
}
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 处理file协议的文件下载
|
||||
private static void handleFileProtocol(URL url, String targetPath) throws IOException {
|
||||
File sourceFile = new File(url.getPath());
|
||||
if (!sourceFile.exists()) {
|
||||
throw new FileNotFoundException("本地文件不存在: " + url.getPath());
|
||||
}
|
||||
if (!sourceFile.isFile()) {
|
||||
throw new IOException("路径不是文件: " + url.getPath());
|
||||
}
|
||||
|
||||
File targetFile = new File(targetPath);
|
||||
|
||||
// 判断源文件和目标文件是否是同一个文件(防止自身复制覆盖)
|
||||
if (isSameFile(sourceFile, targetFile)) {
|
||||
// 如果是同一个文件,直接返回,不执行复制操作
|
||||
logger.info("源文件和目标文件相同,跳过复制: {}", sourceFile.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保目标目录存在
|
||||
File parentDir = targetFile.getParentFile();
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
parentDir.mkdirs();
|
||||
}
|
||||
|
||||
// 复制文件
|
||||
Files.copy(sourceFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个文件是否是同一个文件
|
||||
* 通过比较规范化路径来避免符号链接、相对路径等问题
|
||||
*/
|
||||
private static boolean isSameFile(File file1, File file2) {
|
||||
try {
|
||||
// 使用规范化路径比较,可以处理符号链接、相对路径等情况
|
||||
String canonicalPath1 = file1.getCanonicalPath();
|
||||
String canonicalPath2 = file2.getCanonicalPath();
|
||||
|
||||
// 如果是Windows系统,忽略路径大小写
|
||||
if (isWindows()) {
|
||||
return canonicalPath1.equalsIgnoreCase(canonicalPath2);
|
||||
}
|
||||
return canonicalPath1.equals(canonicalPath2);
|
||||
} catch (IOException e) {
|
||||
// 如果获取规范化路径失败,使用绝对路径比较
|
||||
logger.warn("无法获取文件的规范化路径,使用绝对路径比较: {}, {}", file1.getAbsolutePath(), file2.getAbsolutePath());
|
||||
|
||||
String absolutePath1 = file1.getAbsolutePath();
|
||||
String absolutePath2 = file2.getAbsolutePath();
|
||||
|
||||
if (isWindows()) {
|
||||
return absolutePath1.equalsIgnoreCase(absolutePath2);
|
||||
}
|
||||
return absolutePath1.equals(absolutePath2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取真实文件绝对路径
|
||||
*
|
||||
|
||||
@@ -3,57 +3,227 @@ package cn.keking.utils;
|
||||
import cn.keking.config.ConfigConstants;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.net.ftp.FTPClient;
|
||||
import org.apache.commons.net.ftp.FTPReply;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @auther: chenjh
|
||||
* @since: 2019/6/18 14:36
|
||||
*/
|
||||
public class FtpUtils {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FtpUtils.class);
|
||||
|
||||
public static FTPClient connect(String host, int port, String username, String password, String controlEncoding) throws IOException {
|
||||
/**
|
||||
* 从FTP服务器下载文件到本地
|
||||
*/
|
||||
public static void download(String ftpUrl, String ftpport, String localFilePath,
|
||||
String ftpUsername, String ftpPassword,
|
||||
String ftpControlEncoding) throws IOException {
|
||||
// 获取FTP连接信息
|
||||
FtpConnectionInfo connectionInfo = parseFtpConnectionInfo(ftpUrl, ftpport, ftpUsername, ftpPassword, ftpControlEncoding);
|
||||
|
||||
LOGGER.debug("FTP下载 - url:{}, host:{}, port:{}, username:{}, 保存路径:{}",
|
||||
ftpUrl, connectionInfo.host, connectionInfo.port, connectionInfo.username, localFilePath);
|
||||
|
||||
FTPClient ftpClient = connect(connectionInfo.host, connectionInfo.port,
|
||||
connectionInfo.username, connectionInfo.password,
|
||||
connectionInfo.controlEncoding);
|
||||
|
||||
try {
|
||||
// 设置被动模式
|
||||
ftpClient.enterLocalPassiveMode();
|
||||
|
||||
// 获取文件输入流
|
||||
String encodedFilePath = new String(
|
||||
connectionInfo.remoteFilePath.getBytes(connectionInfo.controlEncoding),
|
||||
StandardCharsets.ISO_8859_1
|
||||
);
|
||||
|
||||
// 方法1:直接下载文件到本地
|
||||
try (OutputStream outputStream = Files.newOutputStream(Paths.get(localFilePath))) {
|
||||
boolean downloadResult = ftpClient.retrieveFile(encodedFilePath, outputStream);
|
||||
LOGGER.debug("FTP下载结果: {}", downloadResult);
|
||||
if (!downloadResult) {
|
||||
throw new IOException("FTP文件下载失败,返回码: " + ftpClient.getReplyCode());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
closeFtpClient(ftpClient);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览FTP文件 - 返回输入流(调用者需要关闭流)
|
||||
*/
|
||||
public static InputStream preview(String ftpUrl, String ftpport, String localFilePath,
|
||||
String ftpUsername, String ftpPassword,
|
||||
String ftpControlEncoding) throws IOException {
|
||||
// 获取FTP连接信息
|
||||
FtpConnectionInfo connectionInfo = parseFtpConnectionInfo(ftpUrl, ftpport, ftpUsername, ftpPassword, ftpControlEncoding);
|
||||
|
||||
LOGGER.debug("FTP预览 - url:{}, host:{}, port:{}, username:{}",
|
||||
ftpUrl, connectionInfo.host, connectionInfo.port, connectionInfo.username);
|
||||
|
||||
FTPClient ftpClient = connect(connectionInfo.host, connectionInfo.port,
|
||||
connectionInfo.username, connectionInfo.password,
|
||||
connectionInfo.controlEncoding);
|
||||
|
||||
try {
|
||||
// 设置被动模式
|
||||
ftpClient.enterLocalPassiveMode();
|
||||
|
||||
// 获取文件输入流
|
||||
String encodedFilePath = new String(
|
||||
connectionInfo.remoteFilePath.getBytes(connectionInfo.controlEncoding),
|
||||
StandardCharsets.ISO_8859_1
|
||||
);
|
||||
|
||||
// 获取文件输入流
|
||||
InputStream inputStream = ftpClient.retrieveFileStream(encodedFilePath);
|
||||
|
||||
if (inputStream == null) {
|
||||
closeFtpClient(ftpClient);
|
||||
throw new IOException("无法获取FTP文件流,可能文件不存在或无权限");
|
||||
}
|
||||
|
||||
// 包装输入流,在流关闭时自动断开FTP连接
|
||||
return new FtpAutoCloseInputStream(inputStream, ftpClient);
|
||||
|
||||
} catch (IOException e) {
|
||||
// 发生异常时确保关闭连接
|
||||
closeFtpClient(ftpClient);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析FTP连接信息(抽取公共逻辑)
|
||||
*/
|
||||
private static FtpConnectionInfo parseFtpConnectionInfo(String ftpUrl, String ftpport,
|
||||
String ftpUsername, String ftpPassword,
|
||||
String ftpControlEncoding) throws IOException {
|
||||
FtpConnectionInfo info = new FtpConnectionInfo();
|
||||
|
||||
// 从配置获取默认连接参数
|
||||
String basic = ConfigConstants.getFtpUsername();
|
||||
if (!StringUtils.isEmpty(basic) && !Objects.equals(basic, "false")) {
|
||||
String[] params = WebUtils.namePass(ftpUrl, basic);
|
||||
if (params != null && params.length >= 5) {
|
||||
info.port = Integer.parseInt(params[1]);
|
||||
info.username = params[2];
|
||||
info.password = params[3];
|
||||
info.controlEncoding = params[4];
|
||||
}
|
||||
}
|
||||
|
||||
// 使用传入参数覆盖默认值
|
||||
if (!StringUtils.isEmpty(ftpport)) {
|
||||
info.port = Integer.parseInt(ftpport);
|
||||
}
|
||||
if (!StringUtils.isEmpty(ftpUsername)) {
|
||||
info.username = ftpUsername;
|
||||
}
|
||||
if (!StringUtils.isEmpty(ftpPassword)) {
|
||||
info.password = ftpPassword;
|
||||
}
|
||||
if (!StringUtils.isEmpty(ftpControlEncoding)) {
|
||||
info.controlEncoding = ftpControlEncoding;
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if (info.port == 0) {
|
||||
info.port = 21;
|
||||
}
|
||||
if (StringUtils.isEmpty(info.controlEncoding)) {
|
||||
info.controlEncoding = "UTF-8";
|
||||
}
|
||||
// 解析URL
|
||||
try {
|
||||
URI uri = new URI(ftpUrl);
|
||||
info.host = uri.getHost();
|
||||
info.remoteFilePath = uri.getPath();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IOException("无效的FTP URL: " + ftpUrl, e);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* FTP连接信息对象
|
||||
*/
|
||||
private static class FtpConnectionInfo {
|
||||
String host;
|
||||
int port = 21;
|
||||
String username;
|
||||
String password;
|
||||
String controlEncoding = "UTF-8";
|
||||
String remoteFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动关闭FTP连接的输入流包装类
|
||||
*/
|
||||
private static class FtpAutoCloseInputStream extends FilterInputStream {
|
||||
private final FTPClient ftpClient;
|
||||
|
||||
protected FtpAutoCloseInputStream(InputStream in, FTPClient ftpClient) {
|
||||
super(in);
|
||||
this.ftpClient = ftpClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
super.close();
|
||||
// 确保FTP命令完成
|
||||
if (ftpClient != null) {
|
||||
ftpClient.completePendingCommand();
|
||||
}
|
||||
} finally {
|
||||
closeFtpClient(ftpClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全关闭FTP连接
|
||||
*/
|
||||
private static void closeFtpClient(FTPClient ftpClient) {
|
||||
if (ftpClient != null && ftpClient.isConnected()) {
|
||||
try {
|
||||
ftpClient.logout();
|
||||
ftpClient.disconnect();
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("关闭FTP连接时发生异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接FTP服务器
|
||||
*/
|
||||
private static FTPClient connect(String host, int port, String username,
|
||||
String password, String controlEncoding) throws IOException {
|
||||
FTPClient ftpClient = new FTPClient();
|
||||
ftpClient.connect(host, port);
|
||||
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
|
||||
ftpClient.login(username, password);
|
||||
}
|
||||
int reply = ftpClient.getReplyCode();
|
||||
if (!FTPReply.isPositiveCompletion(reply)) {
|
||||
ftpClient.disconnect();
|
||||
}
|
||||
ftpClient.setControlEncoding(controlEncoding);
|
||||
ftpClient.connect(host, port);
|
||||
|
||||
if (!ftpClient.login(username, password)) {
|
||||
throw new IOException("FTP登录失败,用户名或密码错误");
|
||||
}
|
||||
|
||||
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
|
||||
return ftpClient;
|
||||
}
|
||||
|
||||
public static void download(String ftpUrl, String localFilePath, String ftpUsername, String ftpPassword, String ftpControlEncoding) throws IOException {
|
||||
String username = StringUtils.isEmpty(ftpUsername) ? ConfigConstants.getFtpUsername() : ftpUsername;
|
||||
String password = StringUtils.isEmpty(ftpPassword) ? ConfigConstants.getFtpPassword() : ftpPassword;
|
||||
String controlEncoding = StringUtils.isEmpty(ftpControlEncoding) ? ConfigConstants.getFtpControlEncoding() : ftpControlEncoding;
|
||||
URL url = new URL(ftpUrl);
|
||||
String host = url.getHost();
|
||||
int port = (url.getPort() == -1) ? url.getDefaultPort() : url.getPort();
|
||||
String remoteFilePath = url.getPath();
|
||||
LOGGER.debug("FTP connection url:{}, username:{}, password:{}, controlEncoding:{}, localFilePath:{}", ftpUrl, username, password, controlEncoding, localFilePath);
|
||||
FTPClient ftpClient = connect(host, port, username, password, controlEncoding);
|
||||
OutputStream outputStream = Files.newOutputStream(Paths.get(localFilePath));
|
||||
ftpClient.enterLocalPassiveMode();
|
||||
boolean downloadResult = ftpClient.retrieveFile(new String(remoteFilePath.getBytes(controlEncoding), StandardCharsets.ISO_8859_1), outputStream);
|
||||
LOGGER.debug("FTP download result {}", downloadResult);
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
ftpClient.logout();
|
||||
ftpClient.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,8 @@ import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class KkFileUtils {
|
||||
|
||||
@@ -19,17 +18,31 @@ public class KkFileUtils {
|
||||
|
||||
public static final String DEFAULT_FILE_ENCODING = "UTF-8";
|
||||
|
||||
private static final List<String> illegalFileStrList = new ArrayList<>();
|
||||
// 路径遍历关键字列表
|
||||
private static final Set<String> illegalFileStrList;
|
||||
|
||||
static {
|
||||
illegalFileStrList.add("../");
|
||||
illegalFileStrList.add("./");
|
||||
illegalFileStrList.add("..\\");
|
||||
illegalFileStrList.add(".\\");
|
||||
illegalFileStrList.add("\\..");
|
||||
illegalFileStrList.add("\\.");
|
||||
illegalFileStrList.add("..");
|
||||
illegalFileStrList.add("...");
|
||||
Set<String> set = new HashSet<>();
|
||||
|
||||
// 基本路径遍历
|
||||
Collections.addAll(set, "../", "./", "..\\", ".\\", "\\..", "\\.", "..", "...", "....", ".....");
|
||||
|
||||
// URL编码的路径遍历
|
||||
Collections.addAll(set, "%2e%2e%2f", "%2e%2e/", "..%2f", "%2e%2e%5c", "%2e%2e\\", "..%5c",
|
||||
"%252e%252e%252f", "%252e%252e/", "..%252f");
|
||||
|
||||
// Unicode编码绕过
|
||||
Collections.addAll(set, "\\u002e\\u002e\\u002f", "\\U002e\\U002e\\U002f",
|
||||
"\u00c0\u00ae\u00c0\u00ae", "\u00c1\u009c\u00c1\u009c");
|
||||
|
||||
// 特殊分隔符
|
||||
Collections.addAll(set, "|..|", "|../|", "|..\\|");
|
||||
|
||||
// Windows特殊路径
|
||||
Collections.addAll(set, "\\\\?\\", "\\\\.\\");
|
||||
|
||||
// 转换为不可变集合
|
||||
illegalFileStrList = Collections.unmodifiableSet(set);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +81,22 @@ public class KkFileUtils {
|
||||
* @return 是否http
|
||||
*/
|
||||
public static boolean isHttpUrl(URL url) {
|
||||
return url.getProtocol().toLowerCase().startsWith("file") || url.getProtocol().toLowerCase().startsWith("http");
|
||||
return url.getProtocol().toLowerCase().startsWith("http") || url.getProtocol().toLowerCase().startsWith("https");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断url是否是file资源
|
||||
*
|
||||
*/
|
||||
public static boolean isFileUrl(URL url) {
|
||||
return url.getProtocol().toLowerCase().startsWith("file");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前操作系统是否为Windows
|
||||
*/
|
||||
static boolean isWindows() {
|
||||
return System.getProperty("os.name").toLowerCase().contains("windows");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,42 +1,90 @@
|
||||
package cn.keking.utils;
|
||||
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
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.HttpClients;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
||||
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
|
||||
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
|
||||
import org.apache.hc.core5.http.io.SocketConfig;
|
||||
import org.apache.hc.core5.util.Timeout;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* @author 鞠玉果
|
||||
* @author 高雄
|
||||
*/
|
||||
public class SslUtils {
|
||||
|
||||
private static void trustAllHttpsCertificates() throws Exception {
|
||||
TrustManager[] trustAllCerts = new TrustManager[1];
|
||||
TrustManager tm = new miTM();
|
||||
trustAllCerts[0] = tm;
|
||||
SSLContext sc = SSLContext.getInstance("SSL");
|
||||
sc.init(null, trustAllCerts, null);
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
|
||||
}
|
||||
/**
|
||||
* 创建忽略SSL验证的HttpClient(适用于HttpClient 5.6)
|
||||
*/
|
||||
public static CloseableHttpClient createHttpClientIgnoreSsl() throws Exception {
|
||||
// 创建自定义的SSL上下文
|
||||
SSLContext sslContext = createIgnoreVerifySSL();
|
||||
|
||||
static class miTM implements TrustManager, X509TrustManager {
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return null;
|
||||
}
|
||||
// 使用SSLConnectionSocketFactoryBuilder构建SSL连接工厂
|
||||
DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy(
|
||||
sslContext, NoopHostnameVerifier.INSTANCE);
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
|
||||
}
|
||||
// 使用新的PoolingHttpClientConnectionManagerBuilder构建连接管理器
|
||||
// 使用连接管理器构建器
|
||||
PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.setTlsSocketStrategy(tlsStrategy)
|
||||
.setDefaultSocketConfig(SocketConfig.custom()
|
||||
.setSoTimeout(Timeout.ofSeconds(10))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException {
|
||||
}
|
||||
// 配置连接池参数
|
||||
connectionManager.setMaxTotal(200);
|
||||
connectionManager.setDefaultMaxPerRoute(20);
|
||||
|
||||
// 配置请求参数
|
||||
RequestConfig requestConfig = RequestConfig.custom()
|
||||
.setConnectionRequestTimeout(Timeout.ofSeconds(10))
|
||||
.setResponseTimeout(Timeout.ofSeconds(72))
|
||||
.setConnectionRequestTimeout(Timeout.ofSeconds(2))
|
||||
.setRedirectsEnabled(true)
|
||||
.setMaxRedirects(5)
|
||||
.build();
|
||||
|
||||
return HttpClients.custom()
|
||||
.setConnectionManager(connectionManager)
|
||||
.setDefaultRequestConfig(requestConfig)
|
||||
.setRedirectStrategy(DefaultRedirectStrategy.INSTANCE)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 忽略HTTPS请求的SSL证书,必须在openConnection之前调用
|
||||
* 创建忽略SSL验证的SSLContext
|
||||
*/
|
||||
public static void ignoreSsl() throws Exception {
|
||||
HostnameVerifier hv = (urlHostName, session) -> true;
|
||||
trustAllHttpsCertificates();
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(hv);
|
||||
}
|
||||
private static SSLContext createIgnoreVerifySSL() throws Exception {
|
||||
// 使用TLSv1.2或TLSv1.3
|
||||
SSLContext sc = SSLContext.getInstance("TLSv1.2");
|
||||
|
||||
// 实现一个X509TrustManager,忽略所有证书验证
|
||||
X509TrustManager trustManager = new X509TrustManager() {
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) {
|
||||
// 信任所有客户端证书
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) {
|
||||
// 信任所有服务器证书
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
};
|
||||
|
||||
sc.init(null, new TrustManager[]{trustManager}, new java.security.SecureRandom());
|
||||
return sc;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
package cn.keking.utils;
|
||||
|
||||
import cn.keking.config.ConfigConstants;
|
||||
import cn.keking.model.FileAttribute;
|
||||
import io.mola.galimatias.GalimatiasParseException;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
@@ -17,8 +21,7 @@ import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -30,6 +33,18 @@ public class WebUtils {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WebUtils.class);
|
||||
private static final String BASE64_MSG = "base64";
|
||||
private static final String URL_PARAM_BASIC_NAME = "basic.name";
|
||||
private static final String URL_PARAM_BASIC_PASS = "basic.pass";
|
||||
private static final Map<String, String> ERROR_MESSAGES = Map.of(
|
||||
"base64", "KK提醒您:接入方法错误未使用BASE64",
|
||||
"base641", "KK提醒您:BASE64解码异常,确认是否正确使用BASE64编码",
|
||||
"Keyerror", "KK提醒您:AES解码错误,请检测你的秘钥是否正确",
|
||||
"base64error", "KK提醒您:你选用的是ASE加密,实际用了BASE64加密接入",
|
||||
"byteerror", "KK提醒您:解码异常,检测你接入方法是否正确"
|
||||
);
|
||||
|
||||
private static final String EMPTY_URL_MSG = "KK提醒您:地址不能为空";
|
||||
private static final String INVALID_URL_MSG = "KK提醒您:请正确使用URL(必须包括https ftp file 协议)";
|
||||
/**
|
||||
* 获取标准的URL
|
||||
*
|
||||
@@ -234,17 +249,18 @@ public class WebUtils {
|
||||
String urls = request.getParameter("urls");
|
||||
String currentUrl = request.getParameter("currentUrl");
|
||||
String urlPath = request.getParameter("urlPath");
|
||||
String encryption = request.getParameter("encryption");
|
||||
if (StringUtils.isNotBlank(url)) {
|
||||
return decodeUrl(url);
|
||||
return decodeUrl(url,encryption);
|
||||
}
|
||||
if (StringUtils.isNotBlank(currentUrl)) {
|
||||
return decodeUrl(currentUrl);
|
||||
return decodeUrl(currentUrl,encryption);
|
||||
}
|
||||
if (StringUtils.isNotBlank(urlPath)) {
|
||||
return decodeUrl(urlPath);
|
||||
return decodeUrl(urlPath,encryption);
|
||||
}
|
||||
if (StringUtils.isNotBlank(urls)) {
|
||||
urls = decodeUrl(urls);
|
||||
urls = decodeUrl(urls,encryption);
|
||||
String[] images = urls.split("\\|");
|
||||
return images[0];
|
||||
}
|
||||
@@ -268,13 +284,20 @@ public class WebUtils {
|
||||
*
|
||||
* aHR0cHM6Ly9maWxlLmtla2luZy5jbi9kZW1vL%2BS4reaWhy5wcHR4 -> https://file.keking.cn/demo/%E4%B8%AD%E6%96%87.pptx -> https://file.keking.cn/demo/中文.pptx
|
||||
*/
|
||||
public static String decodeUrl(String source) {
|
||||
String url = decodeBase64String(source, StandardCharsets.UTF_8);
|
||||
if (! StringUtils.isNotBlank(url)){
|
||||
return null;
|
||||
public static String decodeUrl(String source,String encryption) {
|
||||
String url;
|
||||
if(ObjectUtils.isEmpty(encryption) || Objects.equals(ConfigConstants.getaesKey(), "false")){
|
||||
encryption = "base64";
|
||||
}
|
||||
if(Objects.equals(encryption.toLowerCase(), "aes")){
|
||||
return AESUtil.AesDecrypt(source);
|
||||
}else {
|
||||
url = decodeBase64String(source, StandardCharsets.UTF_8);
|
||||
if(!isValidUrl(url)){
|
||||
url="base641";
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,6 +324,30 @@ public class WebUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static String urlSecurity(String url) {
|
||||
|
||||
if (ObjectUtils.isEmpty(url)) {
|
||||
return EMPTY_URL_MSG;
|
||||
}
|
||||
// 检查已知的错误类型
|
||||
String errorMsg = ERROR_MESSAGES.get(url);
|
||||
if (errorMsg != null) {
|
||||
return errorMsg;
|
||||
}
|
||||
// 验证URL格式
|
||||
if (!isValidUrl(url)) {
|
||||
return INVALID_URL_MSG;
|
||||
}
|
||||
// file协议特殊处理
|
||||
if (url.toLowerCase().startsWith("file://")) {
|
||||
// 对于本地文件,可以返回URL本身或进行特殊处理
|
||||
// 根据业务需求决定:返回URL、返回特殊标识或进行本地文件安全检查
|
||||
return url; // 或者返回特殊标识如 "file-protocol"
|
||||
}
|
||||
// 提取主机名
|
||||
return getHost(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 url 的 host
|
||||
* @param urlStr url
|
||||
@@ -371,4 +418,93 @@ public class WebUtils {
|
||||
}
|
||||
session.removeAttribute(key);
|
||||
}
|
||||
|
||||
public static boolean validateKey(String key) {
|
||||
String configKey = ConfigConstants.getKey();
|
||||
return !"false".equals(configKey) && !configKey.equals(key);
|
||||
}
|
||||
|
||||
public static String getContentTypeByFilename(String filename) {
|
||||
String extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||
switch (extension) {
|
||||
case "pdf": return "application/pdf";
|
||||
case "jpg": case "jpeg": return "image/jpeg";
|
||||
case "png": return "image/png";
|
||||
case "gif": return "image/gif";
|
||||
case "svg": return "image/svg+xml";
|
||||
case "txt": return "text/plain";
|
||||
case "html": case "htm": return "text/html";
|
||||
case "xml": return "application/xml";
|
||||
case "json": return "application/json";
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* name pass 获取用户名 和密码
|
||||
*/
|
||||
public static String[] namePass(String url,String name) {
|
||||
url= getHost(url);
|
||||
String[] items = name.split(",\\s*");
|
||||
String toRemove = ":";
|
||||
String names = null;
|
||||
String[] parts = null;
|
||||
try {
|
||||
for (String item : items) {
|
||||
int index = item.indexOf(toRemove);
|
||||
if (index != -1) {
|
||||
String result = item.substring(0, index);
|
||||
if (Objects.equals(result, url)) {
|
||||
names = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (names !=null){
|
||||
parts = names.split(toRemove);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("获取认证权限错误:",e);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持basic 下载方法
|
||||
*/
|
||||
public static void applyBasicAuthHeaders(HttpHeaders headers, FileAttribute fileAttribute) {
|
||||
String url = fileAttribute.getUrl();
|
||||
// 从配置文件读取User-Agent,如果没有配置则使用默认值
|
||||
String customUserAgent=ConfigConstants.getUserAgent();
|
||||
String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
||||
if (!StringUtils.isEmpty(customUserAgent) && !Objects.equals(customUserAgent, "false")) {
|
||||
userAgent = customUserAgent;
|
||||
}
|
||||
headers.set("User-Agent", userAgent);
|
||||
// 获取用户名和密码
|
||||
String username = null;
|
||||
String password = null;
|
||||
// 从basic配置获取
|
||||
String basic = ConfigConstants.getBasicName();
|
||||
if (!StringUtils.isEmpty(basic) && !Objects.equals(basic, "false")) {
|
||||
String[] urlUser = namePass(url, basic);
|
||||
if (urlUser != null && urlUser.length >= 3) {
|
||||
username = urlUser[1];
|
||||
password = urlUser[2];
|
||||
}
|
||||
}
|
||||
// URL参数优先
|
||||
String basicUsername = getUrlParameterReg(url, URL_PARAM_BASIC_NAME);
|
||||
String basicPassword = getUrlParameterReg(url, URL_PARAM_BASIC_PASS);
|
||||
|
||||
if (!StringUtils.isEmpty(basicUsername)) {
|
||||
username = basicUsername;
|
||||
password = basicPassword;
|
||||
}
|
||||
|
||||
// 设置Basic Auth
|
||||
if (!StringUtils.isEmpty(username)) {
|
||||
String plainCredentials = username + ":" + (password != null ? password : "");
|
||||
String base64Credentials = java.util.Base64.getEncoder().encodeToString(plainCredentials.getBytes());
|
||||
headers.set("Authorization", "Basic " + base64Credentials);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())) {
|
||||
// 支持通配符 * 表示允许所有主机
|
||||
|
||||
@@ -33,6 +33,36 @@
|
||||
<div class="page-header">
|
||||
<h1>版本发布记录</h1>
|
||||
</div>
|
||||
<div class="panel panel-success">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">2025年12月25日,v5.0版本</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div>
|
||||
<h4>优化</h4>
|
||||
1. 优化 OFD 移动端预览 页面不自适应 <br>
|
||||
2. 更新 xlsx 前端解析组件,加速解析速度 <br>
|
||||
3. 升级 CAD 组件 <br>
|
||||
4. office 功能调整,支持批注、转换页码限制、生成水印等功能 <br>
|
||||
5. 升级 markdown 组件 <br>
|
||||
6. 升级 dcm 解析组件 <br>
|
||||
7. 升级 PDF.JS 解析组件 <br>
|
||||
8. 更换视频播放插件为 ckplayer <br>
|
||||
9. tif 解析更加智能化,支持被修改的图片格式 <br>
|
||||
10. 针对大小文本文件检测字符编码的正确率,处理并发隐患 <br>
|
||||
11. 重构下载文件的代码,新增通用的文件服务器认证访问的设计 <br>
|
||||
12. 更新 bootstrap 组件,并精简掉不需要的文件 <br>
|
||||
13. 更新 epub 版本,优化 epub 显示效果 <br>
|
||||
14. 解决定时清除缓存时,对于多媒体类型文件,只删除了磁盘缓存文件 <br>
|
||||
15. 自动检测已安装 Office 组件,增加 LibreOffice 7.5 & 7.6 版本默认路径 <br>
|
||||
16. 修改 drawio 默认为预览模式 <br>
|
||||
17. 新增 PDF 线程管理、超时管理、内存缓存管理,更新 PDF 解析组件版本 <br>
|
||||
18. 优化 Dockerfile,支持真正的跨平台构建镜像 <br>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-success">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">2025年01月16日,v4.4.0版本</h3>
|
||||
|
||||
Reference in New Issue
Block a user