Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions core/init/router/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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"
}
11 changes: 11 additions & 0 deletions core/middleware/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
4 changes: 4 additions & 0 deletions core/middleware/password_expired.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" ||
Expand Down
71 changes: 71 additions & 0 deletions frontend/src/utils/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() || '';
};
10 changes: 2 additions & 8 deletions frontend/src/views/host/file-management/share/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
}
};

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/views/host/file-management/wget/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) => {
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/views/share/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}`);
};
Comment on lines +83 to +90

const resolveBrowserLocale = () => {
if (typeof navigator === 'undefined') {
return 'en';
Expand Down Expand Up @@ -151,6 +160,7 @@ const downloadWithPassword = async () => {

onMounted(async () => {
try {
applySharedPassword();
await applyPublicLocale();
await loadShareInfo();
if (shareInfo.value && !shareInfo.value.hasPassword) {
Expand Down
Loading