From 191ddea309750dddba6b6017bbbdbbd213bae3b5 Mon Sep 17 00:00:00 2001 From: Jacky Date: Sun, 6 Apr 2025 10:55:09 +0000 Subject: [PATCH] feat(config): use encode/decode to handle url #249 --- api/config/add.go | 21 +++- api/config/get.go | 14 +++ api/config/list.go | 27 ++++- api/config/mkdir.go | 19 +++- api/config/modify.go | 15 +++ api/config/rename.go | 25 ++++- app/src/api/curd.ts | 8 +- app/src/api/site.ts | 12 +-- app/src/api/stream.ts | 10 +- app/src/views/config/ConfigEditor.vue | 100 ++++++++++++------ app/src/views/config/ConfigList.vue | 35 ++++-- .../views/config/components/ConfigName.vue | 5 +- app/src/views/config/components/Rename.vue | 4 +- app/src/views/config/configColumns.tsx | 4 +- app/src/views/site/site_edit/SiteEdit.vue | 4 +- .../site/site_edit/components/ConfigName.vue | 2 +- app/src/views/site/site_list/SiteList.vue | 2 +- app/src/views/stream/StreamEdit.vue | 8 +- .../views/stream/components/ConfigName.vue | 2 +- 19 files changed, 235 insertions(+), 82 deletions(-) diff --git a/api/config/add.go b/api/config/add.go index 0a2f00b2..076375a5 100644 --- a/api/config/add.go +++ b/api/config/add.go @@ -2,6 +2,7 @@ package config import ( "net/http" + "net/url" "os" "path/filepath" "time" @@ -28,8 +29,22 @@ func AddConfig(c *gin.Context) { name := json.Name content := json.Content - dir := nginx.GetConfPath(json.BaseDir) - path := filepath.Join(dir, json.Name) + + // Decode paths from URL encoding + decodedBaseDir, err := url.QueryUnescape(json.BaseDir) + if err != nil { + cosy.ErrHandler(c, err) + return + } + + decodedName, err := url.QueryUnescape(name) + if err != nil { + cosy.ErrHandler(c, err) + return + } + + dir := nginx.GetConfPath(decodedBaseDir) + path := filepath.Join(dir, decodedName) if !helper.IsUnderDirectory(path, nginx.GetConfPath()) { c.JSON(http.StatusForbidden, gin.H{ "message": "filepath is not under the nginx conf path", @@ -53,7 +68,7 @@ func AddConfig(c *gin.Context) { } } - err := os.WriteFile(path, []byte(content), 0644) + err = os.WriteFile(path, []byte(content), 0644) if err != nil { cosy.ErrHandler(c, err) return diff --git a/api/config/get.go b/api/config/get.go index 5d47bf95..cd1b0d94 100644 --- a/api/config/get.go +++ b/api/config/get.go @@ -2,6 +2,7 @@ package config import ( "net/http" + "net/url" "os" "path/filepath" @@ -23,6 +24,19 @@ type APIConfigResp struct { func GetConfig(c *gin.Context) { relativePath := c.Param("path") + // Ensure the path is correctly decoded - handle cases where it might be encoded multiple times + decodedPath := relativePath + var err error + // Try decoding until the path no longer changes + for { + newDecodedPath, decodeErr := url.PathUnescape(decodedPath) + if decodeErr != nil || newDecodedPath == decodedPath { + break + } + decodedPath = newDecodedPath + } + relativePath = decodedPath + absPath := nginx.GetConfPath(relativePath) if !helper.IsUnderDirectory(absPath, nginx.GetConfPath()) { c.JSON(http.StatusForbidden, gin.H{ diff --git a/api/config/list.go b/api/config/list.go index a8a08797..2208efb0 100644 --- a/api/config/list.go +++ b/api/config/list.go @@ -2,6 +2,7 @@ package config import ( "net/http" + "net/url" "os" "strings" @@ -16,7 +17,31 @@ func GetConfigs(c *gin.Context) { name := c.Query("name") sortBy := c.Query("sort_by") order := c.DefaultQuery("order", "desc") - dir := c.DefaultQuery("dir", "/") + + // Get directory parameter + encodedDir := c.DefaultQuery("dir", "/") + + // Handle cases where the path might be encoded multiple times + dir := encodedDir + // Try decoding until the path no longer changes + for { + newDecodedDir, decodeErr := url.QueryUnescape(dir) + if decodeErr != nil { + cosy.ErrHandler(c, decodeErr) + return + } + + if newDecodedDir == dir { + break + } + dir = newDecodedDir + } + + // Ensure the directory path format is correct + dir = strings.TrimSpace(dir) + if dir != "/" && strings.HasSuffix(dir, "/") { + dir = strings.TrimSuffix(dir, "/") + } configFiles, err := os.ReadDir(nginx.GetConfPath(dir)) if err != nil { diff --git a/api/config/mkdir.go b/api/config/mkdir.go index 54cba6fd..3996d2cb 100644 --- a/api/config/mkdir.go +++ b/api/config/mkdir.go @@ -2,6 +2,7 @@ package config import ( "net/http" + "net/url" "os" "github.com/0xJacky/Nginx-UI/internal/helper" @@ -18,7 +19,21 @@ func Mkdir(c *gin.Context) { if !cosy.BindAndValid(c, &json) { return } - fullPath := nginx.GetConfPath(json.BasePath, json.FolderName) + + // Ensure paths are properly URL unescaped + decodedBasePath, err := url.QueryUnescape(json.BasePath) + if err != nil { + cosy.ErrHandler(c, err) + return + } + + decodedFolderName, err := url.QueryUnescape(json.FolderName) + if err != nil { + cosy.ErrHandler(c, err) + return + } + + fullPath := nginx.GetConfPath(decodedBasePath, decodedFolderName) if !helper.IsUnderDirectory(fullPath, nginx.GetConfPath()) { c.JSON(http.StatusForbidden, gin.H{ "message": "You are not allowed to create a folder " + @@ -26,7 +41,7 @@ func Mkdir(c *gin.Context) { }) return } - err := os.Mkdir(fullPath, 0755) + err = os.Mkdir(fullPath, 0755) if err != nil { cosy.ErrHandler(c, err) return diff --git a/api/config/modify.go b/api/config/modify.go index 153c1964..a53df2db 100644 --- a/api/config/modify.go +++ b/api/config/modify.go @@ -2,6 +2,7 @@ package config import ( "net/http" + "net/url" "os" "path/filepath" "time" @@ -23,6 +24,20 @@ type EditConfigJson struct { func EditConfig(c *gin.Context) { relativePath := c.Param("path") + + // Ensure the path is correctly decoded - handle cases where it might be encoded multiple times + decodedPath := relativePath + var err error + // Try decoding until the path no longer changes + for { + newDecodedPath, decodeErr := url.PathUnescape(decodedPath) + if decodeErr != nil || newDecodedPath == decodedPath { + break + } + decodedPath = newDecodedPath + } + relativePath = decodedPath + var json struct { Content string `json:"content"` SyncOverwrite bool `json:"sync_overwrite"` diff --git a/api/config/rename.go b/api/config/rename.go index 34b9ca3a..e81a8d12 100644 --- a/api/config/rename.go +++ b/api/config/rename.go @@ -2,6 +2,7 @@ package config import ( "net/http" + "net/url" "os" "path/filepath" "strings" @@ -32,8 +33,28 @@ func Rename(c *gin.Context) { }) return } - origFullPath := nginx.GetConfPath(json.BasePath, json.OrigName) - newFullPath := nginx.GetConfPath(json.BasePath, json.NewName) + + // Decode paths from URL encoding + decodedBasePath, err := url.QueryUnescape(json.BasePath) + if err != nil { + cosy.ErrHandler(c, err) + return + } + + decodedOrigName, err := url.QueryUnescape(json.OrigName) + if err != nil { + cosy.ErrHandler(c, err) + return + } + + decodedNewName, err := url.QueryUnescape(json.NewName) + if err != nil { + cosy.ErrHandler(c, err) + return + } + + origFullPath := nginx.GetConfPath(decodedBasePath, decodedOrigName) + newFullPath := nginx.GetConfPath(decodedBasePath, decodedNewName) if !helper.IsUnderDirectory(origFullPath, nginx.GetConfPath()) || !helper.IsUnderDirectory(newFullPath, nginx.GetConfPath()) { c.JSON(http.StatusForbidden, gin.H{ diff --git a/app/src/api/curd.ts b/app/src/api/curd.ts index c2af9d88..660e84a6 100644 --- a/app/src/api/curd.ts +++ b/app/src/api/curd.ts @@ -44,12 +44,12 @@ class Curd { // eslint-disable-next-line ts/no-explicit-any _get(id: any = null, params: any = {}): Promise { - return http.get(this.baseUrl + (id ? `/${id}` : ''), { params }) + return http.get(this.baseUrl + (id ? `/${encodeURIComponent(id)}` : ''), { params }) } // eslint-disable-next-line ts/no-explicit-any _save(id: any = null, data: any = {}, config: any = undefined): Promise { - return http.post(this.baseUrl + (id ? `/${id}` : ''), data, config) + return http.post(this.baseUrl + (id ? `/${encodeURIComponent(id)}` : ''), data, config) } // eslint-disable-next-line ts/no-explicit-any @@ -69,12 +69,12 @@ class Curd { // eslint-disable-next-line ts/no-explicit-any _destroy(id: any = null, params: any = {}) { - return http.delete(`${this.baseUrl}/${id}`, { params }) + return http.delete(`${this.baseUrl}/${encodeURIComponent(id)}`, { params }) } // eslint-disable-next-line ts/no-explicit-any _recover(id: any = null) { - return http.patch(`${this.baseUrl}/${id}`) + return http.patch(`${this.baseUrl}/${encodeURIComponent(id)}`) } _update_order(data: { target_id: number, direction: number, affected_ids: number[] }) { diff --git a/app/src/api/site.ts b/app/src/api/site.ts index 083d8180..34a740d1 100644 --- a/app/src/api/site.ts +++ b/app/src/api/site.ts @@ -35,7 +35,7 @@ export interface AutoCertRequest { class SiteCurd extends Curd { // eslint-disable-next-line ts/no-explicit-any enable(name: string, config?: any) { - return http.post(`${this.baseUrl}/${name}/enable`, undefined, config) + return http.post(`${this.baseUrl}/${encodeURIComponent(name)}/enable`, undefined, config) } disable(name: string) { @@ -43,7 +43,7 @@ class SiteCurd extends Curd { } rename(oldName: string, newName: string) { - return http.post(`${this.baseUrl}/${oldName}/rename`, { new_name: newName }) + return http.post(`${this.baseUrl}/${encodeURIComponent(oldName)}/rename`, { new_name: newName }) } get_default_template() { @@ -51,19 +51,19 @@ class SiteCurd extends Curd { } add_auto_cert(domain: string, data: AutoCertRequest) { - return http.post(`auto_cert/${domain}`, data) + return http.post(`auto_cert/${encodeURIComponent(domain)}`, data) } remove_auto_cert(domain: string) { - return http.delete(`auto_cert/${domain}`) + return http.delete(`auto_cert/${encodeURIComponent(domain)}`) } duplicate(name: string, data: { name: string }): Promise<{ dst: string }> { - return http.post(`${this.baseUrl}/${name}/duplicate`, data) + return http.post(`${this.baseUrl}/${encodeURIComponent(name)}/duplicate`, data) } advance_mode(name: string, data: { advanced: boolean }) { - return http.post(`${this.baseUrl}/${name}/advance`, data) + return http.post(`${this.baseUrl}/${encodeURIComponent(name)}/advance`, data) } } diff --git a/app/src/api/stream.ts b/app/src/api/stream.ts index 1d5213d2..ad05f6b5 100644 --- a/app/src/api/stream.ts +++ b/app/src/api/stream.ts @@ -21,23 +21,23 @@ export interface Stream { class StreamCurd extends Curd { // eslint-disable-next-line ts/no-explicit-any enable(name: string, config?: any) { - return http.post(`${this.baseUrl}/${name}/enable`, undefined, config) + return http.post(`${this.baseUrl}/${encodeURIComponent(name)}/enable`, undefined, config) } disable(name: string) { - return http.post(`${this.baseUrl}/${name}/disable`) + return http.post(`${this.baseUrl}/${encodeURIComponent(name)}/disable`) } duplicate(name: string, data: { name: string }): Promise<{ dst: string }> { - return http.post(`${this.baseUrl}/${name}/duplicate`, data) + return http.post(`${this.baseUrl}/${encodeURIComponent(name)}/duplicate`, data) } advance_mode(name: string, data: { advanced: boolean }) { - return http.post(`${this.baseUrl}/${name}/advance`, data) + return http.post(`${this.baseUrl}/${encodeURIComponent(name)}/advance`, data) } rename(name: string, newName: string) { - return http.post(`${this.baseUrl}/${name}/rename`, { new_name: newName }) + return http.post(`${this.baseUrl}/${encodeURIComponent(name)}/rename`, { new_name: newName }) } } diff --git a/app/src/views/config/ConfigEditor.vue b/app/src/views/config/ConfigEditor.vue index 2ec16e3d..8e3552cf 100644 --- a/app/src/views/config/ConfigEditor.vue +++ b/app/src/views/config/ConfigEditor.vue @@ -24,10 +24,8 @@ const router = useRouter() // eslint-disable-next-line vue/require-typed-ref const refForm = ref() -const refInspectConfig = useTemplateRef('refInspectConfig') const origName = ref('') const addMode = computed(() => !route.params.name) -const errors = ref({}) const showHistory = ref(false) const basePath = computed(() => { @@ -52,11 +50,15 @@ const activeKey = ref(['basic', 'deploy', 'chatgpt']) const modifiedAt = ref('') const nginxConfigBase = ref('') -const newPath = computed(() => [nginxConfigBase.value, basePath.value, data.value.name] - .filter(v => v) - .join('/')) +const newPath = computed(() => { + // 组合路径后解码显示 + const path = [nginxConfigBase.value, basePath.value, data.value.name] + .filter(v => v) + .join('/') + return path +}) -const relativePath = computed(() => (route.params.name as string[]).join('/')) +const relativePath = computed(() => (basePath.value ? `${basePath.value}/${route.params.name}` : route.params.name) as string) const breadcrumbs = useBreadcrumbs() async function init() { @@ -75,20 +77,26 @@ async function init() { .split('/') .filter(v => v) - const path = filteredPath.map((v, k) => { - let dir = v + // Build accumulated path to maintain original encoding state + let accumulatedPath = '' + const path = filteredPath.map((segment, index) => { + // Decode for display + const decodedSegment = decodeURIComponent(segment) - if (k > 0) { - dir = filteredPath.slice(0, k).join('/') - dir += `/${v}` + // Accumulated path keeps original encoding state + if (index === 0) { + accumulatedPath = segment + } + else { + accumulatedPath = `${accumulatedPath}/${segment}` } return { name: 'Manage Configs', - translatedName: () => v, + translatedName: () => decodedSegment, path: '/config', query: { - dir, + dir: accumulatedPath, }, hasChildren: false, } @@ -116,20 +124,34 @@ async function init() { historyChatgptRecord.value = [] data.value.filepath = '' - const path = basePath.value + const pathSegments = basePath.value .split('/') .filter(v => v) - .map(v => { - return { - name: 'Manage Configs', - translatedName: () => v, - path: '/config', - query: { - dir: v, - }, - hasChildren: false, - } - }) + + // Build accumulated path + let accumulatedPath = '' + const path = pathSegments.map((segment, index) => { + // Decode for display + const decodedSegment = decodeURIComponent(segment) + + // Accumulated path keeps original encoding state + if (index === 0) { + accumulatedPath = segment + } + else { + accumulatedPath = `${accumulatedPath}/${segment}` + } + + return { + name: 'Manage Configs', + translatedName: () => decodedSegment, + path: '/config', + query: { + dir: accumulatedPath, + }, + hasChildren: false, + } + }) breadcrumbs.value = [{ name: 'Dashboard', @@ -167,12 +189,18 @@ function save() { }).then(r => { data.value.content = r.content message.success($gettext('Saved successfully')) - router.push(`/config/${r.filepath.replaceAll(`${nginxConfigBase.value}/`, '')}/edit`) - }).catch(e => { - errors.value = e.errors - message.error($gettext('Save error %{msg}', { msg: e.message ?? '' })) - }).finally(() => { - refInspectConfig.value?.test() + + if (addMode.value) { + router.push({ + path: `/config/${data.value.name}/edit`, + query: { + basePath: basePath.value, + }, + }) + } + else { + data.value = r + } }) }) } @@ -187,10 +215,13 @@ function formatCode() { } function goBack() { + // Keep original path with encoding state + const encodedPath = basePath.value || '' + router.push({ path: '/config', query: { - dir: basePath.value || undefined, + dir: encodedPath || undefined, }, }) } @@ -223,7 +254,6 @@ function openHistory() { @@ -281,14 +311,14 @@ function openHistory() { v-if="!addMode" :label="$gettext('Path')" > - {{ data.filepath }} + {{ decodeURIComponent(data.filepath) }} - {{ newPath }} + {{ decodeURIComponent(newPath) }} v) - const path = filteredPath.map((v, k) => { - let dir = v + let accumulatedPath = '' + const path = filteredPath.map((segment, index) => { + const decodedSegment = decodeURIComponent(segment) - if (k > 0) { - dir = filteredPath.slice(0, k).join('/') - dir += `/${v}` + if (index === 0) { + accumulatedPath = segment + } + else { + accumulatedPath = `${accumulatedPath}/${segment}` } return { name: 'Manage Configs', - translatedName: () => v, + translatedName: () => decodedSegment, path: '/config', query: { - dir, + dir: accumulatedPath, }, hasChildren: false, } @@ -82,10 +85,13 @@ watch(route, () => { }) function goBack() { + const pathSegments = basePath.value.split('/').slice(0, -2) + const encodedPath = pathSegments.length > 0 ? pathSegments.join('/') : '' + router.push({ path: '/config', query: { - dir: `${basePath.value.split('/').slice(0, -2).join('/')}` || undefined, + dir: encodedPath || undefined, }, }) } @@ -144,13 +150,22 @@ const refRename = useTemplateRef('refRename') @click="() => { if (!record.is_dir) { router.push({ - path: `/config/${basePath}${record.name}/edit`, + path: `/config/${encodeURIComponent(record.name)}/edit`, + query: { + basePath, + }, }) } else { + let encodedPath = ''; + if (basePath) { + encodedPath = basePath; + } + encodedPath += encodeURIComponent(record.name); + router.push({ query: { - dir: basePath + record.name, + dir: encodedPath, }, }) } diff --git a/app/src/views/config/components/ConfigName.vue b/app/src/views/config/components/ConfigName.vue index 28606ae5..2b91ef12 100644 --- a/app/src/views/config/components/ConfigName.vue +++ b/app/src/views/config/components/ConfigName.vue @@ -29,7 +29,10 @@ function save() { modify.value = false message.success($gettext('Renamed successfully')) router.push({ - path: `/config/${r.path}/edit`, + path: `/config/${encodeURIComponent(buffer.value)}/edit`, + query: { + basePath: encodeURIComponent(props.dir!), + }, }) }).finally(() => { loading.value = false diff --git a/app/src/views/config/components/Rename.vue b/app/src/views/config/components/Rename.vue index 794039bd..58abcc30 100644 --- a/app/src/views/config/components/Rename.vue +++ b/app/src/views/config/components/Rename.vue @@ -37,9 +37,11 @@ function ok() { const otpModal = use2FAModal() otpModal.open().then(() => { - config.rename(basePath, orig_name, new_name, sync_node_ids).then(() => { + // Note: API will handle URL encoding of path segments + config.rename(basePath, orig_name, new_name, sync_node_ids).then(r => { visible.value = false message.success($gettext('Rename successfully')) + emit('renamed') }) }) diff --git a/app/src/views/config/configColumns.tsx b/app/src/views/config/configColumns.tsx index 857d5de0..81ca5d55 100644 --- a/app/src/views/config/configColumns.tsx +++ b/app/src/views/config/configColumns.tsx @@ -22,10 +22,12 @@ const configColumns = [{ ) } + const displayName = args.text || '' + return (
{renderIcon(args.record.is_dir)} - {args.text} + {displayName}
) }, diff --git a/app/src/views/site/site_edit/SiteEdit.vue b/app/src/views/site/site_edit/SiteEdit.vue index 99419877..a2372d70 100644 --- a/app/src/views/site/site_edit/SiteEdit.vue +++ b/app/src/views/site/site_edit/SiteEdit.vue @@ -19,7 +19,7 @@ import { message } from 'ant-design-vue' const route = useRoute() const router = useRouter() -const name = computed(() => route.params?.name?.toString() ?? '') +const name = computed(() => decodeURIComponent(route.params?.name?.toString() ?? '')) const ngx_config: NgxConfig = reactive({ name: '', @@ -151,7 +151,7 @@ async function save() { }).then(r => { handleResponse(r) router.push({ - path: `/sites/${filename.value}`, + path: `/sites/${encodeURIComponent(filename.value)}`, query: route.query, }) message.success($gettext('Saved successfully')) diff --git a/app/src/views/site/site_edit/components/ConfigName.vue b/app/src/views/site/site_edit/components/ConfigName.vue index 8a8b160b..28263961 100644 --- a/app/src/views/site/site_edit/components/ConfigName.vue +++ b/app/src/views/site/site_edit/components/ConfigName.vue @@ -26,7 +26,7 @@ function save() { modify.value = false message.success($gettext('Renamed successfully')) router.push({ - path: `/sites/${buffer.value}`, + path: `/sites/${encodeURIComponent(buffer.value)}`, }) }).finally(() => { loading.value = false diff --git a/app/src/views/site/site_list/SiteList.vue b/app/src/views/site/site_list/SiteList.vue index cc60cab2..3796c6d8 100644 --- a/app/src/views/site/site_list/SiteList.vue +++ b/app/src/views/site/site_list/SiteList.vue @@ -166,7 +166,7 @@ function handleBatchUpdated() { }" :scroll-x="1600" @click-edit="(r: string) => router.push({ - path: `/sites/${r}`, + path: `/sites/${encodeURIComponent(r)}`, })" @click-batch-modify="handleClickBatchEdit" > diff --git a/app/src/views/stream/StreamEdit.vue b/app/src/views/stream/StreamEdit.vue index ab381945..a7dc67e8 100644 --- a/app/src/views/stream/StreamEdit.vue +++ b/app/src/views/stream/StreamEdit.vue @@ -19,11 +19,7 @@ import { message } from 'ant-design-vue' const route = useRoute() const router = useRouter() -const name = ref(route.params.name.toString()) - -watch(route, () => { - name.value = route.params?.name?.toString() ?? '' -}) +const name = computed(() => decodeURIComponent(route.params?.name?.toString() ?? '')) const ngxConfig: NgxConfig = reactive({ name: '', @@ -139,7 +135,7 @@ async function save() { }).then(r => { handleResponse(r) router.push({ - path: `/streams/${filename.value}`, + path: `/streams/${encodeURIComponent(filename.value)}`, query: route.query, }) message.success($gettext('Saved successfully')) diff --git a/app/src/views/stream/components/ConfigName.vue b/app/src/views/stream/components/ConfigName.vue index 4a43c125..1d6f92c9 100644 --- a/app/src/views/stream/components/ConfigName.vue +++ b/app/src/views/stream/components/ConfigName.vue @@ -26,7 +26,7 @@ function save() { modify.value = false message.success($gettext('Renamed successfully')) router.push({ - path: `/streams/${buffer.value}`, + path: `/streams/${encodeURIComponent(buffer.value)}`, }) }).finally(() => { loading.value = false