fix: Implement file sharing password processing and remote download file name processing (#13452)

This commit is contained in:
2026-08-04 09:37:26 +08:00
committed by GitHub
parent c34ac2f31e
commit b682835b4e
7 changed files with 102 additions and 16 deletions

View File

@@ -40,7 +40,7 @@ func Proxy() gin.HandlerFunc {
apiReq := c.GetBool("API_AUTH")
if !apiReq && !isLocalAPI(reqPath) && !isPublicFileShareAPI(reqPath) && !checkSession(c) {
if !apiReq && !isLocalAPI(reqPath) && !middleware.IsPublicFileShareAPI(reqPath) && !checkSession(c) {
data, _ := res.ErrorMsg.ReadFile("html/401.html")
c.Data(401, "text/html; charset=utf-8", data)
c.Abort()
@@ -97,7 +97,3 @@ func checkSession(c *gin.Context) bool {
func isLocalAPI(urlPath string) bool {
return urlPath == "/api/v2/core/xpack/sync/ssl" || urlPath == "/api/v2/core/xpack/settings/search"
}
func isPublicFileShareAPI(urlPath string) bool {
return urlPath == "/api/v2/files/share/download" || urlPath == "/api/v2/files/share/check" || urlPath == "/api/v2/files/share/info"
}

View File

@@ -11,3 +11,14 @@ func ShouldProxyToAgent(reqPath string) bool {
}
return true
}
func IsPublicFileShareAPI(reqPath string) bool {
switch reqPath {
case "/api/v2/files/share/info",
"/api/v2/files/share/check",
"/api/v2/files/share/download":
return true
default:
return false
}
}

View File

@@ -27,6 +27,10 @@ func PasswordExpired() gin.HandlerFunc {
c.Next()
return
}
if IsPublicFileShareAPI(c.Request.URL.Path) {
c.Next()
return
}
if strings.HasPrefix(c.Request.URL.Path, "/api/v2/core/auth") ||
c.Request.URL.Path == "/api/v2/core/settings/search" ||
c.Request.URL.Path == "/api/v2/core/settings/search/base" ||

View File

@@ -200,3 +200,74 @@ export const resolveEditorLanguage = (path: string, extension = '', name = '') =
return 'yaml';
};
const normalizeUrlFilename = (value: string) => {
let decoded = value.trim().replace(/^['"]|['"]$/g, '');
try {
decoded = decodeURIComponent(decoded);
} catch {
// Keep the original value when it contains an incomplete escape sequence.
}
return (
decoded
.replace(/[\u0000-\u001f\u007f]/g, '')
.split(/[\\/]/)
.pop()
?.trim() || ''
);
};
const getFilenameFromContentDisposition = (contentDisposition: string) => {
const extendedMatch = contentDisposition.match(/(?:^|;)\s*filename\*\s*=\s*(?:"([^"]*)"|([^;]*))/i);
const extendedValue = extendedMatch?.[1] || extendedMatch?.[2]?.trim();
if (extendedValue) {
const encodedValue = extendedValue.match(/^[^']*'[^']*'(.*)$/)?.[1] || extendedValue;
return normalizeUrlFilename(encodedValue);
}
const filenameMatch = contentDisposition.match(/(?:^|;)\s*filename\s*=\s*(?:"([^"]*)"|([^;]*))/i);
return normalizeUrlFilename(filenameMatch?.[1] || filenameMatch?.[2] || '');
};
export const getFilenameFromUrl = (value: string) => {
const normalizedValue = value.trim();
try {
const url = new URL(normalizedValue);
const dispositionKeys = ['response-content-disposition', 'rscd', 'content-disposition'];
for (const key of dispositionKeys) {
const disposition = Array.from(url.searchParams.entries()).find(
([paramKey]) => paramKey.toLowerCase() === key,
)?.[1];
if (disposition) {
const filename = getFilenameFromContentDisposition(disposition);
if (filename) {
return filename;
}
}
}
return normalizeUrlFilename(url.pathname.slice(url.pathname.lastIndexOf('/') + 1));
} catch {
const urlWithoutParams = normalizedValue.replace(/[?#].*$/, '');
return normalizeUrlFilename(urlWithoutParams.slice(urlWithoutParams.lastIndexOf('/') + 1));
}
};
const FILE_SHARE_PASSWORD_KEY = 'k';
export const withFileSharePassword = (value: string, password: string) => {
const shareUrl = new URL(value);
const hashParams = new URLSearchParams(shareUrl.hash.slice(1));
const normalizedPassword = password.trim();
if (normalizedPassword) {
hashParams.set(FILE_SHARE_PASSWORD_KEY, normalizedPassword);
} else {
hashParams.delete(FILE_SHARE_PASSWORD_KEY);
}
shareUrl.hash = hashParams.toString();
return shareUrl.toString();
};
export const getFileSharePasswordFromHash = (hash: string) => {
const hashParams = new URLSearchParams(hash.replace(/^#/, ''));
return hashParams.get(FILE_SHARE_PASSWORD_KEY)?.trim() || '';
};

View File

@@ -100,7 +100,7 @@ import { File } from '@/api/interface/file';
import { CopyDocument, Download, Picture } from '@element-plus/icons-vue';
import i18n from '@/lang';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { buildFileSharePageUrl, buildFileShareQrCodeUrl } from '@/utils/file';
import { buildFileSharePageUrl, buildFileShareQrCodeUrl, withFileSharePassword } from '@/utils/file';
import { copyText } from '@/utils/clipboard';
import { dateFormat as formatDateTime } from '@/utils/date';
import type { FormInstance, FormRules } from 'element-plus';
@@ -266,13 +266,7 @@ const cancelShare = async () => {
const copyLink = () => {
if (shareUrl.value) {
const password = form.sharePassword.trim();
const content = password
? `${i18n.global.t('file.shareLinkLabel')}${shareUrl.value}${i18n.global.t(
'file.sharePassword',
)}${password}`
: `${i18n.global.t('file.shareLinkLabel')}${shareUrl.value}`;
copyText(content);
copyText(withFileSharePassword(shareUrl.value, form.sharePassword));
}
};

View File

@@ -61,6 +61,7 @@ import { FormInstance, FormRules } from 'element-plus';
import { reactive, ref } from 'vue';
import FileList from '@/components/file-list/index.vue';
import { MsgSuccess } from '@/utils/message';
import { getFilenameFromUrl } from '@/utils/file';
interface WgetProps {
path: string;
@@ -143,8 +144,7 @@ const submit = async (formEl: FormInstance | undefined) => {
};
const getFileName = (url: string) => {
const paths = url.split('/');
addForm.name = paths[paths.length - 1];
addForm.name = getFilenameFromUrl(url);
};
const acceptParams = (props: WgetProps) => {

View File

@@ -42,7 +42,7 @@
import { checkFileShare, getPublicFileShareInfo } from '@/api/modules/files';
import { File } from '@/api/interface/file';
import i18n, { loadLocaleMessages } from '@/lang';
import { buildFileShareDownloadUrl } from '@/utils/file';
import { buildFileShareDownloadUrl, getFileSharePasswordFromHash } from '@/utils/file';
import { dateFormat } from '@/utils/date';
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
@@ -80,6 +80,15 @@ const triggerDownload = (pwd = '') => {
window.location.href = buildFileShareDownloadUrl(code.value, currentNode.value, pwd);
};
const applySharedPassword = () => {
const sharedPassword = getFileSharePasswordFromHash(window.location.hash);
if (!sharedPassword) {
return;
}
password.value = sharedPassword;
window.history.replaceState(window.history.state, '', `${window.location.pathname}${window.location.search}`);
};
const resolveBrowserLocale = () => {
if (typeof navigator === 'undefined') {
return 'en';
@@ -151,6 +160,7 @@ const downloadWithPassword = async () => {
onMounted(async () => {
try {
applySharedPassword();
await applyPublicLocale();
await loadShareInfo();
if (shareInfo.value && !shareInfo.value.hasPassword) {