瀏覽代碼

feat(config): use encode/decode to handle url #249

Jacky 3 周之前
父節點
當前提交
191ddea309

+ 18 - 3
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

+ 14 - 0
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{

+ 26 - 1
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 {

+ 17 - 2
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

+ 15 - 0
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"`

+ 23 - 2
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{

+ 4 - 4
app/src/api/curd.ts

@@ -44,12 +44,12 @@ class Curd<T> {
 
   // eslint-disable-next-line ts/no-explicit-any
   _get(id: any = null, params: any = {}): Promise<T> {
-    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<T> {
-    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<T> {
 
   // 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[] }) {

+ 6 - 6
app/src/api/site.ts

@@ -35,7 +35,7 @@ export interface AutoCertRequest {
 class SiteCurd extends Curd<Site> {
   // 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<Site> {
   }
 
   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<Site> {
   }
 
   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)
   }
 }
 

+ 5 - 5
app/src/api/stream.ts

@@ -21,23 +21,23 @@ export interface Stream {
 class StreamCurd extends Curd<Stream> {
   // 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 })
   }
 }
 

+ 65 - 35
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() {
 
         <InspectConfig
           v-show="!addMode"
-          ref="refInspectConfig"
         />
         <CodeEditor v-model:content="data.content" />
         <FooterToolBar>
@@ -281,14 +311,14 @@ function openHistory() {
                 v-if="!addMode"
                 :label="$gettext('Path')"
               >
-                {{ data.filepath }}
+                {{ decodeURIComponent(data.filepath) }}
               </AFormItem>
               <AFormItem
                 v-show="data.name !== origName"
                 :label="addMode ? $gettext('New Path') : $gettext('Changed Path')"
                 required
               >
-                {{ newPath }}
+                {{ decodeURIComponent(newPath) }}
               </AFormItem>
               <AFormItem
                 v-if="!addMode"

+ 25 - 10
app/src/views/config/ConfigList.vue

@@ -40,20 +40,23 @@ function updateBreadcrumbs() {
     .split('/')
     .filter(v => 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,
                 },
               })
             }

+ 4 - 1
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

+ 3 - 1
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')
       })
     })

+ 3 - 1
app/src/views/config/configColumns.tsx

@@ -22,10 +22,12 @@ const configColumns = [{
       )
     }
 
+    const displayName = args.text || ''
+
     return (
       <div class="flex">
         {renderIcon(args.record.is_dir)}
-        {args.text}
+        {displayName}
       </div>
     )
   },

+ 2 - 2
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'))

+ 1 - 1
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

+ 1 - 1
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"
     >

+ 2 - 6
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'))

+ 1 - 1
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