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

This commit is contained in:
Jacky 2025-04-06 10:55:09 +00:00
parent 4b8d26cf5b
commit 191ddea309
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
19 changed files with 235 additions and 82 deletions

View file

@ -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

View file

@ -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{

View file

@ -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 {

View file

@ -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

View file

@ -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"`

View file

@ -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{

View file

@ -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[] }) {

View file

@ -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)
}
}

View file

@ -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 })
}
}

View file

@ -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"

View file

@ -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,
},
})
}

View file

@ -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

View file

@ -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')
})
})

View file

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

View file

@ -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'))

View file

@ -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

View file

@ -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"
>

View file

@ -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'))

View file

@ -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