From 40274d8c92249043d8cddb6e40b4b23128e18df4 Mon Sep 17 00:00:00 2001 From: barkure <43804451+barkure@users.noreply.github.com> Date: Thu, 23 Jul 2026 22:02:49 +0800 Subject: [PATCH 01/23] web/frpc: support virtual net visitor plugin (#5414) --- web/frpc/components.d.ts | 1 + web/frpc/src/components/ConfigField.vue | 30 ++++++++++- .../visitor-form/VisitorConnectionSection.vue | 6 ++- .../visitor-form/VisitorFormLayout.vue | 6 +++ .../visitor-form/VisitorPluginSection.vue | 52 +++++++++++++++++++ web/frpc/src/types/proxy-converters.ts | 19 +++++++ web/frpc/src/types/proxy-form.ts | 7 +++ web/frpc/src/views/VisitorEdit.vue | 12 +++++ 8 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 web/frpc/src/components/visitor-form/VisitorPluginSection.vue diff --git a/web/frpc/components.d.ts b/web/frpc/components.d.ts index c58d0bca..675f376d 100644 --- a/web/frpc/components.d.ts +++ b/web/frpc/components.d.ts @@ -38,6 +38,7 @@ declare module 'vue' { VisitorBaseSection: typeof import('./src/components/visitor-form/VisitorBaseSection.vue')['default'] VisitorConnectionSection: typeof import('./src/components/visitor-form/VisitorConnectionSection.vue')['default'] VisitorFormLayout: typeof import('./src/components/visitor-form/VisitorFormLayout.vue')['default'] + VisitorPluginSection: typeof import('./src/components/visitor-form/VisitorPluginSection.vue')['default'] VisitorTransportSection: typeof import('./src/components/visitor-form/VisitorTransportSection.vue')['default'] VisitorXtcpSection: typeof import('./src/components/visitor-form/VisitorXtcpSection.vue')['default'] } diff --git a/web/frpc/src/components/ConfigField.vue b/web/frpc/src/components/ConfigField.vue index 27719847..67be4ba0 100644 --- a/web/frpc/src/components/ConfigField.vue +++ b/web/frpc/src/components/ConfigField.vue @@ -12,7 +12,7 @@ diff --git a/web/frpc/src/types/proxy-converters.ts b/web/frpc/src/types/proxy-converters.ts index 6587986e..eb4c4c17 100644 --- a/web/frpc/src/types/proxy-converters.ts +++ b/web/frpc/src/types/proxy-converters.ts @@ -190,6 +190,16 @@ export function formToStoreVisitor(form: VisitorFormData): VisitorDefinition { block.bindPort = form.bindPort } + if ( + form.pluginType === 'virtual_net' && + (form.type === 'stcp' || form.type === 'xtcp') + ) { + block.plugin = { + type: 'virtual_net', + destinationIP: form.pluginDestinationIP.trim(), + } + } + if (form.type === 'xtcp') { if (form.protocol && form.protocol !== 'quic') { block.protocol = form.protocol @@ -448,6 +458,15 @@ export function storeVisitorToForm( form.bindAddr = c.bindAddr || '127.0.0.1' form.bindPort = c.bindPort + // Visitor plugin (only supported by stcp/xtcp; sudp runtime never uses it) + if ( + c.plugin?.type === 'virtual_net' && + (form.type === 'stcp' || form.type === 'xtcp') + ) { + form.pluginType = 'virtual_net' + form.pluginDestinationIP = c.plugin.destinationIP || '' + } + // XTCP specific form.protocol = c.protocol || 'quic' form.keepTunnelOpen = c.keepTunnelOpen || false diff --git a/web/frpc/src/types/proxy-form.ts b/web/frpc/src/types/proxy-form.ts index 8f6bd614..793fe516 100644 --- a/web/frpc/src/types/proxy-form.ts +++ b/web/frpc/src/types/proxy-form.ts @@ -79,6 +79,10 @@ export interface VisitorFormData { bindAddr: string bindPort: number | undefined + // Visitor plugin + pluginType: '' | 'virtual_net' + pluginDestinationIP: string + // XTCP specific (XTCPVisitorConfig) protocol: string keepTunnelOpen: boolean @@ -156,6 +160,9 @@ export function createDefaultVisitorForm(): VisitorFormData { bindAddr: '127.0.0.1', bindPort: undefined, + pluginType: '', + pluginDestinationIP: '', + protocol: 'quic', keepTunnelOpen: false, maxRetriesAnHour: undefined, diff --git a/web/frpc/src/views/VisitorEdit.vue b/web/frpc/src/views/VisitorEdit.vue index 615e97e9..bf92e29e 100644 --- a/web/frpc/src/views/VisitorEdit.vue +++ b/web/frpc/src/views/VisitorEdit.vue @@ -107,6 +107,18 @@ const formRules: FormRules = { trigger: 'blur', }, ], + pluginDestinationIP: [ + { + validator: (_rule, value, callback) => { + if (form.value.pluginType === 'virtual_net' && !value?.trim()) { + callback(new Error('Destination IP is required for virtual_net')) + return + } + callback() + }, + trigger: 'blur', + }, + ], } const goBack = () => { From 2d63f6b1f90549c420d3d16e68eff2ab21c78308 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 27 Jul 2026 12:04:00 +0800 Subject: [PATCH 02/23] web/frpc: fix visitor plugin validation for sudp (#5448) --- web/frpc/src/views/VisitorEdit.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/frpc/src/views/VisitorEdit.vue b/web/frpc/src/views/VisitorEdit.vue index bf92e29e..f095103d 100644 --- a/web/frpc/src/views/VisitorEdit.vue +++ b/web/frpc/src/views/VisitorEdit.vue @@ -110,7 +110,11 @@ const formRules: FormRules = { pluginDestinationIP: [ { validator: (_rule, value, callback) => { - if (form.value.pluginType === 'virtual_net' && !value?.trim()) { + if ( + form.value.pluginType === 'virtual_net' && + (form.value.type === 'stcp' || form.value.type === 'xtcp') && + !value?.trim() + ) { callback(new Error('Destination IP is required for virtual_net')) return } From de4b4837104df37cb274800c316a06603d722df5 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 27 Jul 2026 14:57:58 +0800 Subject: [PATCH 03/23] web: patch vulnerable transitive dependencies (#5449) --- web/package-lock.json | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 8ef6716a..541273e9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2473,15 +2473,16 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/braces": { @@ -4998,13 +4999,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -5047,9 +5048,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -5669,9 +5670,9 @@ } }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "funding": [ { "type": "opencollective", @@ -5686,8 +5687,9 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, From 2175557d4813ab8805c76ea0e04fe1d11d688f91 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 27 Jul 2026 19:21:28 +0800 Subject: [PATCH 04/23] web: fix Vite config tsconfig includes (#5450) --- web/frpc/tsconfig.node.json | 2 +- web/frpc/vite.config.mts | 5 ----- web/frps/tsconfig.node.json | 2 +- web/frps/vite.config.mts | 5 ----- 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/web/frpc/tsconfig.node.json b/web/frpc/tsconfig.node.json index 42872c59..a8583534 100644 --- a/web/frpc/tsconfig.node.json +++ b/web/frpc/tsconfig.node.json @@ -6,5 +6,5 @@ "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.mts"] } diff --git a/web/frpc/vite.config.mts b/web/frpc/vite.config.mts index 6a9205d0..209a91fd 100644 --- a/web/frpc/vite.config.mts +++ b/web/frpc/vite.config.mts @@ -28,15 +28,10 @@ export default defineConfig({ '@shared': fileURLToPath(new URL('../shared', import.meta.url)), }, dedupe: ['vue', 'element-plus', '@element-plus/icons-vue'], - modules: [ - fileURLToPath(new URL('../node_modules', import.meta.url)), - 'node_modules', - ], }, css: { preprocessorOptions: { scss: { - api: 'modern', additionalData: `@use "@shared/css/_index.scss" as *;`, }, }, diff --git a/web/frps/tsconfig.node.json b/web/frps/tsconfig.node.json index 42872c59..a8583534 100644 --- a/web/frps/tsconfig.node.json +++ b/web/frps/tsconfig.node.json @@ -6,5 +6,5 @@ "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.mts"] } diff --git a/web/frps/vite.config.mts b/web/frps/vite.config.mts index 91f099f8..fda910d4 100644 --- a/web/frps/vite.config.mts +++ b/web/frps/vite.config.mts @@ -28,15 +28,10 @@ export default defineConfig({ '@shared': fileURLToPath(new URL('../shared', import.meta.url)), }, dedupe: ['vue', 'element-plus', '@element-plus/icons-vue'], - modules: [ - fileURLToPath(new URL('../node_modules', import.meta.url)), - 'node_modules', - ], }, css: { preprocessorOptions: { scss: { - api: 'modern', additionalData: `@use "@shared/css/_index.scss" as *;`, }, }, From 00c24e39bbe14a4ef697d69604391bb46d46b347 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 28 Jul 2026 01:27:35 +0800 Subject: [PATCH 05/23] test(web): add frontend unit test baseline (#5451) --- .circleci/config.yml | 15 +- .github/workflows/golangci-lint.yml | 8 +- Makefile | 5 +- web/frpc/test/config-field.test.ts | 73 ++ web/frpc/test/proxy-converters.test.ts | 96 ++ web/frps/test/stat-card.test.ts | 48 + web/package-lock.json | 1508 +++++++++++++++++++++++- web/package.json | 11 +- web/shared/test/action-button.test.ts | 29 + web/test/setup.test.ts | 25 + web/test/setup.ts | 25 + web/vitest.config.mts | 40 + 12 files changed, 1866 insertions(+), 17 deletions(-) create mode 100644 web/frpc/test/config-field.test.ts create mode 100644 web/frpc/test/proxy-converters.test.ts create mode 100644 web/frps/test/stat-card.test.ts create mode 100644 web/shared/test/action-button.test.ts create mode 100644 web/test/setup.test.ts create mode 100644 web/test/setup.ts create mode 100644 web/vitest.config.mts diff --git a/.circleci/config.yml b/.circleci/config.yml index 21f4159d..7a61cbb8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,14 +7,15 @@ jobs: steps: - checkout - run: - name: Build web assets (frps) - command: make install build - working_directory: web/frps + name: Test and build web assets + command: make web-ci - run: - name: Build web assets (frpc) - command: make install build - working_directory: web/frpc - - run: make + name: Check Go formatting and build binaries + command: | + set -e + make env fmt + git diff --exit-code + make build - run: make alltest workflows: diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 3217c234..36e8d50b 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -22,12 +22,8 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '22' - - name: Build web assets (frps) - run: make build - working-directory: web/frps - - name: Build web assets (frpc) - run: make build - working-directory: web/frpc + - name: Test and build web assets + run: make web-ci - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: diff --git a/Makefile b/Makefile index bac5601a..79c25f85 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ NOWEB_TAG = $(shell [ ! -d web/frps/dist ] || [ ! -d web/frpc/dist ] && echo ',n FRP_COMPAT_BASELINE_COUNT ?= 8 FRP_COMPAT_FLOOR_VERSION ?= 0.61.0 -.PHONY: web frps-web frpc-web frps frpc e2e-compatibility-smoke e2e-compatibility e2e-compatibility-floor +.PHONY: web web-ci frps-web frpc-web frps frpc e2e-compatibility-smoke e2e-compatibility e2e-compatibility-floor all: env fmt web build @@ -16,6 +16,9 @@ env: web: frps-web frpc-web +web-ci: + cd web && npm ci && npm run test:unit && npm run build --workspace frps && npm run build --workspace frpc + frps-web: $(MAKE) -C web/frps build diff --git a/web/frpc/test/config-field.test.ts b/web/frpc/test/config-field.test.ts new file mode 100644 index 00000000..64986274 --- /dev/null +++ b/web/frpc/test/config-field.test.ts @@ -0,0 +1,73 @@ +import { defineComponent } from 'vue' +import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import ConfigField from '../src/components/ConfigField.vue' + +const InputStub = defineComponent({ + props: ['modelValue', 'disabled', 'placeholder', 'type'], + emits: ['update:modelValue'], + template: + '', +}) + +const FormItemStub = defineComponent({ + template: '
', +}) + +const stubs = { + 'el-form-item': FormItemStub, + 'el-input': InputStub, + 'el-switch': defineComponent({ + props: ['modelValue', 'disabled'], + emits: ['update:modelValue'], + template: '', + }), + KeyValueEditor: defineComponent({ template: '
' }), + StringListEditor: defineComponent({ template: '
' }), + PopoverMenu: defineComponent({ template: '
' }), + PopoverMenuItem: defineComponent({ template: '
' }), +} + +describe('ConfigField', () => { + it('clamps numeric input and emits the public model update', async () => { + const wrapper = mount(ConfigField, { + props: { label: 'Port', type: 'number', modelValue: 100, min: 1, max: 65535 }, + global: { stubs }, + }) + + const input = wrapper.find('input') + await input.setValue('70000') + + expect(wrapper.emitted('update:modelValue')).toContainEqual([65535]) + expect((input.element as HTMLInputElement).value).toBe('65535') + }) + + it('keeps a leading sign as an editable draft until it becomes numeric', async () => { + const wrapper = mount(ConfigField, { + props: { label: 'Port', type: 'number', modelValue: 100 }, + global: { stubs }, + }) + + const input = wrapper.find('input') + await wrapper.setProps({ modelValue: 200 }) + expect((input.element as HTMLInputElement).value).toBe('200') + + await input.setValue('-') + + expect(wrapper.emitted('update:modelValue')).toContainEqual([undefined]) + await wrapper.setProps({ modelValue: undefined }) + expect((input.element as HTMLInputElement).value).toBe('-') + }) + + it('renders an empty readonly field as a disabled placeholder', () => { + const wrapper = mount(ConfigField, { + props: { label: 'Secret', readonly: true, modelValue: '' }, + global: { stubs }, + }) + + const input = wrapper.find('input').element as HTMLInputElement + expect(wrapper.find('.config-field-label').text()).toBe('Secret') + expect(input.disabled).toBe(true) + expect(input.value).toBe('—') + }) +}) diff --git a/web/frpc/test/proxy-converters.test.ts b/web/frpc/test/proxy-converters.test.ts new file mode 100644 index 00000000..69e3410b --- /dev/null +++ b/web/frpc/test/proxy-converters.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest' +import { + createDefaultProxyForm, + createDefaultVisitorForm, +} from '../src/types/proxy-form' +import { + formToStoreProxy, + formToStoreVisitor, + storeProxyToForm, +} from '../src/types/proxy-converters' + +describe('proxy converters', () => { + it('serializes only configured proxy fields into the typed store block', () => { + const form = createDefaultProxyForm() + Object.assign(form, { + name: 'web', + type: 'http', + enabled: false, + localIP: '10.0.0.8', + localPort: 8080, + useEncryption: true, + customDomains: ['example.com', ''], + locations: ['/api', ''], + metadatas: [{ key: 'team', value: 'edge' }], + }) + + expect(formToStoreProxy(form)).toEqual({ + name: 'web', + type: 'http', + http: { + enabled: false, + localIP: '10.0.0.8', + localPort: 8080, + transport: { useEncryption: true }, + metadatas: { team: 'edge' }, + customDomains: ['example.com'], + locations: ['/api'], + }, + }) + }) + + it('round-trips store values while applying form defaults', () => { + const form = storeProxyToForm({ + name: 'tcp-proxy', + type: 'tcp', + tcp: { + localPort: 9000, + customDomains: 'unused-for-tcp', + enabled: false, + metadatas: { owner: 'platform' }, + }, + }) + + expect(form).toMatchObject({ + name: 'tcp-proxy', + type: 'tcp', + enabled: false, + localIP: '127.0.0.1', + localPort: 9000, + metadatas: [{ key: 'owner', value: 'platform' }], + multiplexer: 'httpconnect', + }) + }) + + it.each(['stcp', 'xtcp'] as const)( + 'serializes virtual_net for %s visitors', + (type) => { + const form = createDefaultVisitorForm() + Object.assign(form, { + name: 'visitor', + type, + pluginType: 'virtual_net', + pluginDestinationIP: ' 192.0.2.10 ', + bindPort: 6000, + }) + + expect(formToStoreVisitor(form)[type]).toMatchObject({ + plugin: { type: 'virtual_net', destinationIP: '192.0.2.10' }, + bindPort: 6000, + }) + }, + ) + + it('does not serialize virtual_net for SUDP visitors', () => { + const form = createDefaultVisitorForm() + Object.assign(form, { + name: 'visitor', + type: 'sudp', + pluginType: 'virtual_net', + pluginDestinationIP: '192.0.2.10', + bindPort: 6000, + }) + + expect(formToStoreVisitor(form).sudp).toEqual({ bindPort: 6000 }) + }) +}) diff --git a/web/frps/test/stat-card.test.ts b/web/frps/test/stat-card.test.ts new file mode 100644 index 00000000..e5f4cbb6 --- /dev/null +++ b/web/frps/test/stat-card.test.ts @@ -0,0 +1,48 @@ +import { defineComponent } from 'vue' +import { describe, expect, it, vi } from 'vitest' +import { mount } from '@vue/test-utils' + +const router = vi.hoisted(() => ({ push: vi.fn() })) +vi.mock('vue-router', () => ({ useRouter: () => router })) + +import StatCard from '../src/components/StatCard.vue' + +const stubs = { + 'el-card': defineComponent({ + template: '
', + emits: ['click'], + }), + 'el-icon': defineComponent({ template: '' }), +} + +describe('StatCard', () => { + it('shows its content and does not navigate without a destination', async () => { + router.push.mockClear() + const wrapper = mount(StatCard, { + props: { label: 'Clients', value: 3, subtitle: 'active' }, + global: { stubs }, + }) + + expect(wrapper.find('.stat-value').text()).toBe('3') + expect(wrapper.find('.stat-label').text()).toBe('Clients') + expect(wrapper.find('.stat-subtitle').text()).toBe('active') + expect(wrapper.find('.arrow-icon').exists()).toBe(false) + + await wrapper.find('.stat-card').trigger('click') + + expect(router.push).not.toHaveBeenCalled() + }) + + it('navigates only when a destination is provided', async () => { + router.push.mockClear() + const wrapper = mount(StatCard, { + props: { label: 'Proxies', value: '12', type: 'proxies', to: '/proxies' }, + global: { stubs }, + }) + + await wrapper.find('.stat-card').trigger('click') + + expect(router.push).toHaveBeenCalledWith('/proxies') + expect(wrapper.find('.arrow-icon').exists()).toBe(true) + }) +}) diff --git a/web/package-lock.json b/web/package-lock.json index 541273e9..aaa13dce 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,7 +9,12 @@ "shared", "frpc", "frps" - ] + ], + "devDependencies": { + "@vue/test-utils": "2.4.11", + "jsdom": "29.1.1", + "vitest": "4.1.10" + } }, "frpc": { "name": "frpc-dashboard", @@ -82,6 +87,78 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -128,6 +205,155 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@bramus/specificity/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@bramus/specificity/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@ctrl/tinycolor": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", @@ -755,6 +981,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@floating-ui/core": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", @@ -832,6 +1076,24 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -938,6 +1200,7 @@ "hasInstallScript": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", @@ -980,6 +1243,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -1001,6 +1265,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -1022,6 +1287,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -1043,6 +1309,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -1064,6 +1331,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -1085,6 +1353,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -1106,6 +1375,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -1127,6 +1397,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -1148,6 +1419,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -1169,6 +1441,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -1190,6 +1463,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -1211,6 +1485,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -1232,6 +1507,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -1246,6 +1522,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=12" }, @@ -1253,6 +1530,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -1669,6 +1957,31 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1975,6 +2288,129 @@ "vue": "^3.2.25" } }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@volar/language-core": { "version": "2.4.28", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", @@ -2218,6 +2654,190 @@ "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", "license": "MIT" }, + "node_modules/@vue/test-utils": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.11.tgz", + "integrity": "sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^3.0.0" + }, + "peerDependencies": { + "@vue/compiler-dom": "3.x", + "@vue/server-renderer": "3.x", + "vue": "3.x" + }, + "peerDependenciesMeta": { + "@vue/server-renderer": { + "optional": true + } + } + }, + "node_modules/@vue/test-utils/node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/test-utils/node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@vue/test-utils/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/test-utils/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vue/test-utils/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@vue/test-utils/node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@vue/test-utils/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue/test-utils/node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@vue/test-utils/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vue/test-utils/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue/test-utils/node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@vue/test-utils/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@vue/tsconfig": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.8.1.tgz", @@ -2325,6 +2945,19 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2401,6 +3034,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -2443,6 +3086,16 @@ "node": "18 || 20 || >=22" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2661,6 +3314,16 @@ "node": ">=6" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2745,6 +3408,17 @@ "dev": true, "license": "MIT" }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", @@ -2755,6 +3429,13 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", @@ -2884,6 +3565,20 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2962,6 +3657,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3025,6 +3727,7 @@ "dev": true, "license": "Apache-2.0", "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -3116,6 +3819,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/element-plus": { "version": "2.13.5", "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.5.tgz", @@ -3183,6 +3893,13 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -3773,6 +4490,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -3928,6 +4655,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/frp-shared": { "resolved": "shared", "link": true @@ -4251,6 +4995,19 @@ "dev": true, "license": "ISC" }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4295,6 +5052,13 @@ "node": ">=0.8.19" } }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4491,6 +5255,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4577,6 +5351,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4748,6 +5529,22 @@ "dev": true, "license": "ISC" }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -4758,6 +5555,13 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -4787,6 +5591,103 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/jsdom/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/jsdom/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4933,6 +5834,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5014,6 +5925,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -5085,7 +6006,8 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/node-fetch-native": { "version": "1.6.7", @@ -5423,6 +6345,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", @@ -5498,6 +6434,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5525,6 +6468,32 @@ "node": ">=4" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -5750,6 +6719,13 @@ "node": ">=6.0.0" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5882,6 +6858,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -6101,6 +7087,19 @@ "node": ">=11.0.0" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scule": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", @@ -6281,6 +7280,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6356,6 +7375,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6370,6 +7403,70 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.padend": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", @@ -6448,6 +7545,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6558,6 +7695,13 @@ "node": ">= 10" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/synckit": { "version": "0.11.12", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", @@ -6593,6 +7737,13 @@ "node": ">=10" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", @@ -6650,6 +7801,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", + "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.9" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", + "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6663,6 +7844,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6895,6 +8102,16 @@ "node": ">=18.12.0" } }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -7496,6 +8713,109 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -7524,6 +8844,13 @@ } } }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.8.tgz", + "integrity": "sha512-troqCMmQodQDqUqn63NQaFi+CDSclSe7sc8VEBFqf5GFLqmGR2Ph3P2WEC7qwpRVyEWsTi/aAr4vyOe/B1hU3g==", + "dev": true, + "license": "MIT" + }, "node_modules/vue-eslint-parser": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz", @@ -7599,6 +8926,39 @@ "typescript": ">=5.0.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -7606,6 +8966,31 @@ "dev": true, "license": "MIT" }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7711,6 +9096,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7721,6 +9123,101 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", @@ -7731,6 +9228,13 @@ "node": ">=12" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/web/package.json b/web/package.json index 5df16a59..78a8461e 100644 --- a/web/package.json +++ b/web/package.json @@ -1,5 +1,14 @@ { "name": "frp-web", "private": true, - "workspaces": ["shared", "frpc", "frps"] + "workspaces": ["shared", "frpc", "frps"], + "scripts": { + "test:unit": "vitest run", + "test:unit:watch": "vitest" + }, + "devDependencies": { + "@vue/test-utils": "2.4.11", + "jsdom": "29.1.1", + "vitest": "4.1.10" + } } diff --git a/web/shared/test/action-button.test.ts b/web/shared/test/action-button.test.ts new file mode 100644 index 00000000..a38629da --- /dev/null +++ b/web/shared/test/action-button.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import ActionButton from '../components/ActionButton.vue' + +describe('ActionButton', () => { + it('emits click events for an enabled button', async () => { + const wrapper = mount(ActionButton, { slots: { default: 'Save' } }) + + await wrapper.trigger('click') + + expect(wrapper.emitted('click')).toHaveLength(1) + expect(wrapper.text()).toBe('Save') + }) + + it('disables itself and shows loading text while loading', async () => { + const wrapper = mount(ActionButton, { + props: { loading: true, loadingText: 'Saving...' }, + slots: { default: 'Save' }, + }) + + await wrapper.trigger('click') + + expect((wrapper.element as HTMLButtonElement).disabled).toBe(true) + expect(wrapper.classes()).toContain('is-loading') + expect(wrapper.find('.spinner').exists()).toBe(true) + expect(wrapper.text()).toBe('Saving...') + expect(wrapper.emitted('click')).toBeUndefined() + }) +}) diff --git a/web/test/setup.test.ts b/web/test/setup.test.ts new file mode 100644 index 00000000..7fef29e1 --- /dev/null +++ b/web/test/setup.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it, vi } from 'vitest' + +const capturedFetch = globalThis.fetch + +describe('unit-test environment', () => { + it('blocks real network requests before test modules are evaluated', async () => { + await expect(capturedFetch('https://example.invalid/')).rejects.toThrow( + 'Real network requests are disabled in unit tests', + ) + }) + + it('allows a test to replace fetch explicitly', async () => { + const replacement = vi.fn(async () => new Response('ok')) + vi.stubGlobal('fetch', replacement) + + await expect(fetch('/test-endpoint')).resolves.toBeInstanceOf(Response) + expect(replacement).toHaveBeenCalledWith('/test-endpoint') + }) + + it('restores the network guard after a test replacement', async () => { + await expect(fetch('/must-remain-isolated')).rejects.toThrow( + 'Real network requests are disabled in unit tests', + ) + }) +}) diff --git a/web/test/setup.ts b/web/test/setup.ts new file mode 100644 index 00000000..01421df0 --- /dev/null +++ b/web/test/setup.ts @@ -0,0 +1,25 @@ +import { enableAutoUnmount } from '@vue/test-utils' +import { afterAll, afterEach, vi } from 'vitest' + +const originalFetch = globalThis.fetch +const blockedFetch = vi.fn(async () => { + throw new Error('Real network requests are disabled in unit tests') +}) + +globalThis.fetch = blockedFetch as typeof fetch + +afterEach(() => { + vi.restoreAllMocks() + vi.unstubAllGlobals() + vi.clearAllMocks() + globalThis.fetch = blockedFetch as typeof fetch + document.body.innerHTML = '' +}) + +// Vitest runs afterEach hooks in reverse registration order, so wrappers are +// unmounted before the cleanup hook above clears the DOM. +enableAutoUnmount(afterEach) + +afterAll(() => { + globalThis.fetch = originalFetch +}) diff --git a/web/vitest.config.mts b/web/vitest.config.mts new file mode 100644 index 00000000..a3ecafc1 --- /dev/null +++ b/web/vitest.config.mts @@ -0,0 +1,40 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + projects: [ + { + extends: './frpc/vite.config.mts', + root: './frpc', + test: { + name: 'frpc', + environment: 'jsdom', + css: true, + server: { + deps: { inline: ['element-plus', '@element-plus/icons-vue'] }, + }, + setupFiles: ['../test/setup.ts'], + include: [ + '../test/**/*.test.ts', + 'test/**/*.test.ts', + '../shared/**/*.test.ts', + ], + }, + }, + { + extends: './frps/vite.config.mts', + root: './frps', + test: { + name: 'frps', + environment: 'jsdom', + css: true, + server: { + deps: { inline: ['element-plus', '@element-plus/icons-vue'] }, + }, + setupFiles: ['../test/setup.ts'], + include: ['test/**/*.test.ts'], + }, + }, + ], + }, +}) From f3828cbfdbd623de9ed227517e027e5d8bccc69f Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 28 Jul 2026 16:10:29 +0800 Subject: [PATCH 06/23] build(web): upgrade ESLint to v10 (#5452) --- Makefile | 2 +- web/frpc/package.json | 7 +- web/frps/package.json | 7 +- web/package-lock.json | 600 ++++++++---------------------------------- 4 files changed, 122 insertions(+), 494 deletions(-) diff --git a/Makefile b/Makefile index 79c25f85..a22bce7d 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ env: web: frps-web frpc-web web-ci: - cd web && npm ci && npm run test:unit && npm run build --workspace frps && npm run build --workspace frpc + cd web && npm ci && npm run lint:check --workspace frps && npm run lint:check --workspace frpc && npm run test:unit && npm run build --workspace frps && npm run build --workspace frpc frps-web: $(MAKE) -C web/frps build diff --git a/web/frpc/package.json b/web/frpc/package.json index 6e20d893..c13d855f 100644 --- a/web/frpc/package.json +++ b/web/frpc/package.json @@ -9,7 +9,8 @@ "preview": "vite preview", "build-only": "vite build", "type-check": "vue-tsc --noEmit", - "lint": "eslint --fix" + "lint": "eslint . --fix", + "lint:check": "eslint ." }, "dependencies": { "element-plus": "^2.13.0", @@ -24,8 +25,8 @@ "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", "@vueuse/core": "^14.1.0", - "eslint": "^9.39.0", - "eslint-plugin-vue": "^9.33.0", + "eslint": "^10.8.0", + "eslint-plugin-vue": "^10.10.0", "npm-run-all": "^4.1.5", "prettier": "^3.7.4", "sass": "^1.97.2", diff --git a/web/frps/package.json b/web/frps/package.json index c44c0dd4..a90914ad 100644 --- a/web/frps/package.json +++ b/web/frps/package.json @@ -9,7 +9,8 @@ "preview": "vite preview", "build-only": "vite build", "type-check": "vue-tsc --noEmit", - "lint": "eslint --fix" + "lint": "eslint . --fix", + "lint:check": "eslint ." }, "dependencies": { "element-plus": "^2.13.0", @@ -23,8 +24,8 @@ "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", "@vueuse/core": "^14.1.0", - "eslint": "^9.39.0", - "eslint-plugin-vue": "^9.33.0", + "eslint": "^10.8.0", + "eslint-plugin-vue": "^10.10.0", "npm-run-all": "^4.1.5", "prettier": "^3.7.4", "sass": "^1.97.2", diff --git a/web/package-lock.json b/web/package-lock.json index aaa13dce..7e5cf341 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -32,8 +32,8 @@ "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", "@vueuse/core": "^14.1.0", - "eslint": "^9.39.0", - "eslint-plugin-vue": "^9.33.0", + "eslint": "^10.8.0", + "eslint-plugin-vue": "^10.10.0", "npm-run-all": "^4.1.5", "prettier": "^3.7.4", "sass": "^1.97.2", @@ -62,8 +62,8 @@ "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", "@vueuse/core": "^14.1.0", - "eslint": "^9.39.0", - "eslint-plugin-vue": "^9.33.0", + "eslint": "^10.8.0", + "eslint-plugin-vue": "^10.10.0", "npm-run-all": "^4.1.5", "prettier": "^3.7.4", "sass": "^1.97.2", @@ -818,167 +818,68 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", - "minimatch": "^3.1.5" + "minimatch": "^10.2.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0" + "@eslint/core": "^1.2.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@exodus/bytes": { @@ -1982,6 +1883,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2988,13 +2896,6 @@ "node": ">= 8" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -3304,16 +3205,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -3324,23 +3215,6 @@ "node": ">=18" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -4128,33 +4002,33 @@ } }, "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", - "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -4164,8 +4038,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -4173,7 +4046,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -4235,117 +4108,52 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", - "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==", + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.10.0.tgz", + "integrity": "sha512-dL9x9rBHqqNcByWiLOHK6L0SB97V82/NC0cZRn9cXPjM7pCuWlpQQP9bFH4vjBv80ej1ZpzAkuD8zWH1o9bZbA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "globals": "^13.24.0", + "@eslint-community/eslint-utils": "^4.9.1", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", - "postcss-selector-parser": "^6.0.15", - "semver": "^7.6.3", - "vue-eslint-parser": "^9.4.3", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-vue/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-vue/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-vue/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-vue/node_modules/vue-eslint-parser": { - "version": "9.4.3", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", - "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.6" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "postcss-selector-parser": "^7.1.4", + "semver": "^7.8.5", + "xml-name-validator": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, + "peerDependencies": { + "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "vue-eslint-parser": "^10.3.0" + }, + "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, + "@typescript-eslint/parser": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, "funding": { "url": "https://opencollective.com/eslint" } @@ -4363,76 +4171,45 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -4838,19 +4615,6 @@ "node": ">=10.13.0" } }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -4901,16 +4665,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -5025,23 +4779,6 @@ "dev": true, "license": "MIT" }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -5569,28 +5306,6 @@ "dev": true, "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsdom": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", @@ -5678,16 +5393,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/jsdom/node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5827,13 +5532,6 @@ "lodash-es": "*" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "11.5.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", @@ -6441,19 +6139,6 @@ "dev": true, "license": "BlueOak-1.0.0" }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -6667,9 +6352,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "dev": true, "license": "MIT", "dependencies": { @@ -6889,16 +6574,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -7108,9 +6783,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -7595,19 +7270,6 @@ "node": ">=4" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-literal": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", @@ -7633,19 +7295,6 @@ "node": ">=16" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -7896,19 +7545,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -8939,16 +8575,6 @@ "node": ">=18" } }, - "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -9219,13 +8845,13 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xmlchars": { From 3d89ab7ff11dfc593436809eebf650c2d16d102b Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 28 Jul 2026 17:09:46 +0800 Subject: [PATCH 07/23] build(web): upgrade auto import plugins (#5453) --- web/frpc/auto-imports.d.ts | 1 + web/frpc/components.d.ts | 8 +- web/frpc/package.json | 4 +- web/frps/auto-imports.d.ts | 1 + web/frps/components.d.ts | 8 +- web/frps/package.json | 4 +- web/package-lock.json | 566 ++++++++++++++----------------------- 7 files changed, 238 insertions(+), 354 deletions(-) diff --git a/web/frpc/auto-imports.d.ts b/web/frpc/auto-imports.d.ts index 1d89ee8c..9d240079 100644 --- a/web/frpc/auto-imports.d.ts +++ b/web/frpc/auto-imports.d.ts @@ -3,6 +3,7 @@ // @ts-nocheck // noinspection JSUnusedGlobalSymbols // Generated by unplugin-auto-import +// biome-ignore lint: disable export {} declare global { diff --git a/web/frpc/components.d.ts b/web/frpc/components.d.ts index 675f376d..262d5831 100644 --- a/web/frpc/components.d.ts +++ b/web/frpc/components.d.ts @@ -1,10 +1,14 @@ /* eslint-disable */ -/* prettier-ignore */ // @ts-nocheck +// biome-ignore lint: disable +// oxlint-disable +// ------ // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 + export {} +/* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { ConfigField: typeof import('./src/components/ConfigField.vue')['default'] @@ -42,7 +46,7 @@ declare module 'vue' { VisitorTransportSection: typeof import('./src/components/visitor-form/VisitorTransportSection.vue')['default'] VisitorXtcpSection: typeof import('./src/components/visitor-form/VisitorXtcpSection.vue')['default'] } - export interface ComponentCustomProperties { + export interface GlobalDirectives { vLoading: typeof import('element-plus/es')['ElLoadingDirective'] } } diff --git a/web/frpc/package.json b/web/frpc/package.json index c13d855f..5b1f853c 100644 --- a/web/frpc/package.json +++ b/web/frpc/package.json @@ -32,9 +32,9 @@ "sass": "^1.97.2", "terser": "^5.44.1", "typescript": "^5.9.3", - "unplugin-auto-import": "^0.17.5", + "unplugin-auto-import": "^21.0.0", "unplugin-element-plus": "^0.11.2", - "unplugin-vue-components": "^0.26.0", + "unplugin-vue-components": "^32.1.0", "vite": "^7.3.0", "vite-svg-loader": "^5.1.0", "vue-tsc": "^3.2.2" diff --git a/web/frps/auto-imports.d.ts b/web/frps/auto-imports.d.ts index 1d89ee8c..9d240079 100644 --- a/web/frps/auto-imports.d.ts +++ b/web/frps/auto-imports.d.ts @@ -3,6 +3,7 @@ // @ts-nocheck // noinspection JSUnusedGlobalSymbols // Generated by unplugin-auto-import +// biome-ignore lint: disable export {} declare global { diff --git a/web/frps/components.d.ts b/web/frps/components.d.ts index cd53c58f..e177245b 100644 --- a/web/frps/components.d.ts +++ b/web/frps/components.d.ts @@ -1,10 +1,14 @@ /* eslint-disable */ -/* prettier-ignore */ // @ts-nocheck +// biome-ignore lint: disable +// oxlint-disable +// ------ // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 + export {} +/* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { ClientCard: typeof import('./src/components/ClientCard.vue')['default'] @@ -25,7 +29,7 @@ declare module 'vue' { StatCard: typeof import('./src/components/StatCard.vue')['default'] Traffic: typeof import('./src/components/Traffic.vue')['default'] } - export interface ComponentCustomProperties { + export interface GlobalDirectives { vLoading: typeof import('element-plus/es')['ElLoadingDirective'] } } diff --git a/web/frps/package.json b/web/frps/package.json index a90914ad..ba25dd91 100644 --- a/web/frps/package.json +++ b/web/frps/package.json @@ -31,9 +31,9 @@ "sass": "^1.97.2", "terser": "^5.44.1", "typescript": "^5.9.3", - "unplugin-auto-import": "^0.17.5", + "unplugin-auto-import": "^21.0.0", "unplugin-element-plus": "^0.11.2", - "unplugin-vue-components": "^0.26.0", + "unplugin-vue-components": "^32.1.0", "vite": "^7.3.0", "vite-svg-loader": "^5.1.0", "vue-tsc": "^3.2.2" diff --git a/web/package-lock.json b/web/package-lock.json index 7e5cf341..4bada8f5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -39,9 +39,9 @@ "sass": "^1.97.2", "terser": "^5.44.1", "typescript": "^5.9.3", - "unplugin-auto-import": "^0.17.5", + "unplugin-auto-import": "^21.0.0", "unplugin-element-plus": "^0.11.2", - "unplugin-vue-components": "^0.26.0", + "unplugin-vue-components": "^32.1.0", "vite": "^7.3.0", "vite-svg-loader": "^5.1.0", "vue-tsc": "^3.2.2" @@ -69,24 +69,14 @@ "sass": "^1.97.2", "terser": "^5.44.1", "typescript": "^5.9.3", - "unplugin-auto-import": "^0.17.5", + "unplugin-auto-import": "^21.0.0", "unplugin-element-plus": "^0.11.2", - "unplugin-vue-components": "^0.26.0", + "unplugin-vue-components": "^32.1.0", "vite": "^7.3.0", "vite-svg-loader": "^5.1.0", "vue-tsc": "^3.2.2" } }, - "node_modules/@antfu/utils": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", - "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/@asamuzakjp/css-color": { "version": "5.1.11", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", @@ -1473,41 +1463,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", @@ -2882,20 +2837,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -2997,19 +2938,6 @@ "require-from-string": "^2.0.2" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/birpc": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", @@ -4872,19 +4800,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -5479,14 +5394,15 @@ } }, "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", "dev": true, "license": "MIT", "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" }, "engines": { "node": ">=14" @@ -5495,6 +5411,25 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/local-pkg/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5640,9 +5575,9 @@ "license": "MIT" }, "node_modules/mlly": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz", - "integrity": "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", "dev": true, "license": "MIT", "dependencies": { @@ -5737,16 +5672,6 @@ "semver": "bin/semver" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/normalize-wheel-es": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", @@ -7271,9 +7196,9 @@ } }, "node_modules/strip-literal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", - "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", "dev": true, "license": "MIT", "dependencies": { @@ -7404,14 +7329,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -7710,34 +7635,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/unctx/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/unctx/node_modules/unplugin": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", - "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "acorn": "^8.15.0", - "picomatch": "^4.0.3", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": ">=18.12.0" - } - }, "node_modules/undici": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", @@ -7756,26 +7653,29 @@ "license": "MIT" }, "node_modules/unimport": { - "version": "3.14.6", - "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.14.6.tgz", - "integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-5.7.0.tgz", + "integrity": "sha512-njnL6sp8lEA8QQbZrt+52p/g4X0rw3bnGGmUcJnt1jeG8+iiqO779aGz0PirCtydAIVcuTBRlJ52F0u46z309Q==", "dev": true, "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.1.4", - "acorn": "^8.14.0", + "acorn": "^8.16.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", - "fast-glob": "^3.3.3", - "local-pkg": "^1.0.0", - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "pathe": "^2.0.1", - "picomatch": "^4.0.2", - "pkg-types": "^1.3.0", + "local-pkg": "^1.1.2", + "magic-string": "^0.30.21", + "mlly": "^1.8.0", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "pkg-types": "^2.3.0", "scule": "^1.3.0", - "strip-literal": "^2.1.1", - "unplugin": "^1.16.1" + "strip-literal": "^3.1.0", + "tinyglobby": "^0.2.15", + "unplugin": "^2.3.11", + "unplugin-utils": "^0.3.1" + }, + "engines": { + "node": ">=18.12.0" } }, "node_modules/unimport/node_modules/confbox": { @@ -7808,41 +7708,12 @@ "@types/estree": "^1.0.0" } }, - "node_modules/unimport/node_modules/local-pkg": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", - "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mlly": "^1.7.4", - "pkg-types": "^2.3.0", - "quansync": "^0.2.11" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } - }, "node_modules/unimport/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -7850,44 +7721,56 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/unplugin": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", - "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "node_modules/unimport/node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.14.0", + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.12.0" } }, "node_modules/unplugin-auto-import": { - "version": "0.17.8", - "resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-0.17.8.tgz", - "integrity": "sha512-CHryj6HzJ+n4ASjzwHruD8arhbdl+UXvhuAIlHDs15Y/IMecG3wrf7FVg4pVH/DIysbq/n0phIjNHAjl7TG7Iw==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-21.0.0.tgz", + "integrity": "sha512-vWuC8SwqJmxZFYwPojhOhOXDb5xFhNNcEVb9K/RFkyk/3VnfaOjzitWN7v+8DEKpMjSsY2AEGXNgt6I0yQrhRQ==", "dev": true, "license": "MIT", "dependencies": { - "@antfu/utils": "^0.7.10", - "@rollup/pluginutils": "^5.1.0", - "fast-glob": "^3.3.2", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.10", - "minimatch": "^9.0.4", - "unimport": "^3.7.2", - "unplugin": "^1.11.0" + "local-pkg": "^1.1.2", + "magic-string": "^0.30.21", + "picomatch": "^4.0.3", + "unimport": "^5.6.0", + "unplugin": "^2.3.11", + "unplugin-utils": "^0.3.1" }, "engines": { - "node": ">=14" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "@nuxt/kit": "^3.2.2", + "@nuxt/kit": "^4.0.0", "@vueuse/core": "*" }, "peerDependenciesMeta": { @@ -7899,37 +7782,17 @@ } } }, - "node_modules/unplugin-auto-import/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/unplugin-auto-import/node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "node_modules/unplugin-auto-import/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/unplugin-auto-import/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/unplugin-element-plus": { @@ -8011,18 +7874,6 @@ "node": ">= 4" } }, - "node_modules/unplugin-element-plus/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/unplugin-element-plus/node_modules/pkg-types": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", @@ -8035,155 +7886,178 @@ "pathe": "^2.0.3" } }, - "node_modules/unplugin-element-plus/node_modules/unplugin": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", - "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "node_modules/unplugin-utils": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz", + "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "acorn": "^8.15.0", - "picomatch": "^4.0.3", - "webpack-virtual-modules": "^0.6.2" + "pathe": "^2.0.3", + "picomatch": "^4.0.4" }, "engines": { - "node": ">=18.12.0" + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/unplugin-utils/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/unplugin-vue-components": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.26.0.tgz", - "integrity": "sha512-s7IdPDlnOvPamjunVxw8kNgKNK8A5KM1YpK5j/p97jEKTjlPNrA0nZBiSfAKKlK1gWZuyWXlKL5dk3EDw874LQ==", + "version": "32.1.0", + "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-32.1.0.tgz", + "integrity": "sha512-YiUkSxuRjab18XFOrX5VsIxXzccrfmHVGsGeJgSgklb829DQmCy9E4vvDUE4tuvZZdxyFJZX0Oc4TPnnxiiMyg==", "dev": true, "license": "MIT", "dependencies": { - "@antfu/utils": "^0.7.6", - "@rollup/pluginutils": "^5.0.4", - "chokidar": "^3.5.3", - "debug": "^4.3.4", - "fast-glob": "^3.3.1", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.3", - "minimatch": "^9.0.3", - "resolve": "^1.22.4", - "unplugin": "^1.4.0" + "chokidar": "^5.0.0", + "local-pkg": "^1.2.0", + "magic-string": "^0.30.21", + "mlly": "^1.8.2", + "obug": "^2.1.1", + "picomatch": "^4.0.4", + "tinyglobby": "^0.2.16", + "unplugin": "^3.0.0", + "unplugin-utils": "^0.3.1" }, "engines": { - "node": ">=14" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "@babel/parser": "^7.15.8", - "@nuxt/kit": "^3.2.2", - "vue": "2 || 3" + "@nuxt/kit": "^3.2.2 || ^4.0.0", + "vue": "^3.0.0" }, "peerDependenciesMeta": { - "@babel/parser": { - "optional": true - }, "@nuxt/kit": { "optional": true } } }, - "node_modules/unplugin-vue-components/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/unplugin-vue-components/node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/unplugin-vue-components/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, - "node_modules/unplugin-vue-components/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/unplugin-vue-components/node_modules/local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "node_modules/unplugin-vue-components/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { - "node": ">=14" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/unplugin-vue-components/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/unplugin-vue-components/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/unplugin-vue-components/node_modules/unplugin": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz", + "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==", "dev": true, "license": "MIT", "dependencies": { - "picomatch": "^2.2.1" + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.4", + "webpack-virtual-modules": "^0.6.2" }, "engines": { - "node": ">=8.10.0" + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/unplugin/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/untyped": { From 5c6d761c1287e6153f07b824fb6d71b96ee598fe Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 28 Jul 2026 19:36:44 +0800 Subject: [PATCH 08/23] chore(web): upgrade frontend dependencies (#5454) --- web/frpc/package.json | 16 +- web/frps/package.json | 16 +- web/package-lock.json | 776 ++++++++++++++++++++++++++---------------- web/package.json | 3 +- 4 files changed, 499 insertions(+), 312 deletions(-) diff --git a/web/frpc/package.json b/web/frpc/package.json index 5b1f853c..17e8c594 100644 --- a/web/frpc/package.json +++ b/web/frpc/package.json @@ -13,10 +13,10 @@ "lint:check": "eslint ." }, "dependencies": { - "element-plus": "^2.13.0", + "element-plus": "^2.14.3", "pinia": "^3.0.4", - "vue": "^3.5.26", - "vue-router": "^4.6.4" + "vue": "^3.5.40", + "vue-router": "^5.2.0" }, "devDependencies": { "@types/node": "24", @@ -24,19 +24,19 @@ "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", - "@vueuse/core": "^14.1.0", + "@vueuse/core": "^14.3.0", "eslint": "^10.8.0", "eslint-plugin-vue": "^10.10.0", "npm-run-all": "^4.1.5", - "prettier": "^3.7.4", - "sass": "^1.97.2", - "terser": "^5.44.1", + "prettier": "^3.9.6", + "sass": "^1.102.0", + "terser": "^5.49.0", "typescript": "^5.9.3", "unplugin-auto-import": "^21.0.0", "unplugin-element-plus": "^0.11.2", "unplugin-vue-components": "^32.1.0", "vite": "^7.3.0", "vite-svg-loader": "^5.1.0", - "vue-tsc": "^3.2.2" + "vue-tsc": "^3.3.8" } } diff --git a/web/frps/package.json b/web/frps/package.json index ba25dd91..8cc42d50 100644 --- a/web/frps/package.json +++ b/web/frps/package.json @@ -13,9 +13,9 @@ "lint:check": "eslint ." }, "dependencies": { - "element-plus": "^2.13.0", - "vue": "^3.5.26", - "vue-router": "^4.6.4" + "element-plus": "^2.14.3", + "vue": "^3.5.40", + "vue-router": "^5.2.0" }, "devDependencies": { "@types/node": "24", @@ -23,19 +23,19 @@ "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", - "@vueuse/core": "^14.1.0", + "@vueuse/core": "^14.3.0", "eslint": "^10.8.0", "eslint-plugin-vue": "^10.10.0", "npm-run-all": "^4.1.5", - "prettier": "^3.7.4", - "sass": "^1.97.2", - "terser": "^5.44.1", + "prettier": "^3.9.6", + "sass": "^1.102.0", + "terser": "^5.49.0", "typescript": "^5.9.3", "unplugin-auto-import": "^21.0.0", "unplugin-element-plus": "^0.11.2", "unplugin-vue-components": "^32.1.0", "vite": "^7.3.0", "vite-svg-loader": "^5.1.0", - "vue-tsc": "^3.2.2" + "vue-tsc": "^3.3.8" } } diff --git a/web/package-lock.json b/web/package-lock.json index 4bada8f5..95ef7757 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,17 +13,18 @@ "devDependencies": { "@vue/test-utils": "2.4.11", "jsdom": "29.1.1", - "vitest": "4.1.10" + "vitest": "4.1.10", + "vue": "^3.5.40" } }, "frpc": { "name": "frpc-dashboard", "version": "0.0.1", "dependencies": { - "element-plus": "^2.13.0", + "element-plus": "^2.14.3", "pinia": "^3.0.4", - "vue": "^3.5.26", - "vue-router": "^4.6.4" + "vue": "^3.5.40", + "vue-router": "^5.2.0" }, "devDependencies": { "@types/node": "24", @@ -31,29 +32,29 @@ "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", - "@vueuse/core": "^14.1.0", + "@vueuse/core": "^14.3.0", "eslint": "^10.8.0", "eslint-plugin-vue": "^10.10.0", "npm-run-all": "^4.1.5", - "prettier": "^3.7.4", - "sass": "^1.97.2", - "terser": "^5.44.1", + "prettier": "^3.9.6", + "sass": "^1.102.0", + "terser": "^5.49.0", "typescript": "^5.9.3", "unplugin-auto-import": "^21.0.0", "unplugin-element-plus": "^0.11.2", "unplugin-vue-components": "^32.1.0", "vite": "^7.3.0", "vite-svg-loader": "^5.1.0", - "vue-tsc": "^3.2.2" + "vue-tsc": "^3.3.8" } }, "frps": { "name": "frps-dashboard", "version": "0.0.1", "dependencies": { - "element-plus": "^2.13.0", - "vue": "^3.5.26", - "vue-router": "^4.6.4" + "element-plus": "^2.14.3", + "vue": "^3.5.40", + "vue-router": "^5.2.0" }, "devDependencies": { "@types/node": "24", @@ -61,20 +62,20 @@ "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", - "@vueuse/core": "^14.1.0", + "@vueuse/core": "^14.3.0", "eslint": "^10.8.0", "eslint-plugin-vue": "^10.10.0", "npm-run-all": "^4.1.5", - "prettier": "^3.7.4", - "sass": "^1.97.2", - "terser": "^5.44.1", + "prettier": "^3.9.6", + "sass": "^1.102.0", + "terser": "^5.49.0", "typescript": "^5.9.3", "unplugin-auto-import": "^21.0.0", "unplugin-element-plus": "^0.11.2", "unplugin-vue-components": "^32.1.0", "vite": "^7.3.0", "vite-svg-loader": "^5.1.0", - "vue-tsc": "^3.2.2" + "vue-tsc": "^3.3.8" } }, "node_modules/@asamuzakjp/css-color": { @@ -149,31 +150,94 @@ "dev": true, "license": "MIT" }, + "node_modules/@babel/generator": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", + "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "@types/jsesc": "^2.5.0", + "jsesc": "^3.0.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/helper-string-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz", + "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==", + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/types": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -183,13 +247,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -989,7 +1053,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1000,7 +1063,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1011,7 +1073,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1038,7 +1099,6 @@ "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1849,7 +1909,13 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", "license": "MIT" }, "node_modules/@types/json-schema": { @@ -1888,7 +1954,6 @@ "version": "0.0.21", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", - "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -2303,14 +2368,41 @@ "vscode-uri": "^3.0.8" } }, - "node_modules/@vue/compiler-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", - "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "node_modules/@vue-macros/common": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.4.tgz", + "integrity": "sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.30", + "@vue/compiler-sfc": "^3.5.22", + "ast-kit": "^2.1.2", + "local-pkg": "^1.1.2", + "magic-string-ast": "^1.0.2", + "unplugin-utils": "^0.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/vue-macros" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.40", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" @@ -2329,40 +2421,40 @@ } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", - "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", - "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.30", - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30", + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.8", + "postcss": "^8.5.19", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", - "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" } }, "node_modules/@vue/devtools-api": { @@ -2440,26 +2532,27 @@ } }, "node_modules/@vue/language-core": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz", - "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.8.tgz", + "integrity": "sha512-ieGT8jJdhhy0mGzStZhsg/qPw5bQZJg5yF+3+XU6saf4sM7yo9ZXy3h+nCwrm2+b4qS/SypkNdR2jAF3uei9tA==", "dev": true, "license": "MIT", "dependencies": { "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", - "alien-signals": "^3.0.0", + "alien-signals": "^3.2.1", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", - "picomatch": "^4.0.2" + "picomatch": "^4.0.4" } }, "node_modules/@vue/language-core/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -2468,53 +2561,51 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", - "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.30" + "@vue/shared": "3.5.40" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", - "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", - "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/runtime-core": "3.5.30", - "@vue/shared": "3.5.30", + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", - "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30" - }, - "peerDependencies": { - "vue": "3.5.30" + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" } }, "node_modules/@vue/shared": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", - "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", "license": "MIT" }, "node_modules/@vue/test-utils": { @@ -2721,15 +2812,14 @@ } }, "node_modules/@vueuse/core": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", - "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", - "dev": true, + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.2.1", - "@vueuse/shared": "14.2.1" + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -2739,20 +2829,18 @@ } }, "node_modules/@vueuse/metadata": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz", - "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", - "dev": true, + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/shared": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz", - "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==", - "dev": true, + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -2765,7 +2853,6 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2802,9 +2889,9 @@ } }, "node_modules/alien-signals": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", - "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", "dev": true, "license": "MIT" }, @@ -2886,6 +2973,39 @@ "node": ">=12" } }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.9.0.tgz", + "integrity": "sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@babel/types": "^7.29.0", + "ast-kit": "^2.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -3016,22 +3136,6 @@ } } }, - "node_modules/c12/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/c12/node_modules/confbox": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", @@ -3069,20 +3173,6 @@ "destr": "^2.0.3" } }, - "node_modules/c12/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -3144,16 +3234,15 @@ } }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -3207,7 +3296,6 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, "license": "MIT" }, "node_modules/config-chain": { @@ -3629,70 +3717,29 @@ "license": "MIT" }, "node_modules/element-plus": { - "version": "2.13.5", - "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.5.tgz", - "integrity": "sha512-dmY24fhSREfZN/PuUt0YZigMso7wWzl+B5o+YKNN15kQIn/0hzamsPU+ebj9SES0IbUqsLX1wkrzYmzU8VrVOQ==", + "version": "2.14.3", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.3.tgz", + "integrity": "sha512-pJcvxcpZjYruNzuJhAeVwnbYjfNgzBKnWHwSVEhwzM2/kcLI3brzmtIBxtPqd4hQWJfD1PRnjoc1WipLw2eBGg==", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^4.2.0", "@element-plus/icons-vue": "^2.3.2", - "@floating-ui/dom": "^1.0.1", - "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", - "@types/lodash": "^4.17.20", + "@floating-ui/dom": "^1.7.6", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", "@types/lodash-es": "^4.17.12", - "@vueuse/core": "12.0.0", + "@vueuse/core": "14.3.0", "async-validator": "^4.2.5", - "dayjs": "^1.11.19", - "lodash": "^4.17.23", - "lodash-es": "^4.17.23", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", "lodash-unified": "^1.0.3", "memoize-one": "^6.0.0", - "normalize-wheel-es": "^1.2.0" + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.5" }, "peerDependencies": { - "vue": "^3.3.0" - } - }, - "node_modules/element-plus/node_modules/@types/web-bluetooth": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", - "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", - "license": "MIT" - }, - "node_modules/element-plus/node_modules/@vueuse/core": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz", - "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "12.0.0", - "@vueuse/shared": "12.0.0", - "vue": "^3.5.13" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/element-plus/node_modules/@vueuse/metadata": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz", - "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/element-plus/node_modules/@vueuse/shared": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz", - "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==", - "license": "MIT", - "dependencies": { - "vue": "^3.5.13" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "vue": "^3.3.7" } }, "node_modules/emoji-regex": { @@ -3879,7 +3926,7 @@ "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -4209,7 +4256,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "dev": true, "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -5308,6 +5354,18 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5336,6 +5394,18 @@ "dev": true, "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5397,7 +5467,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", - "dev": true, "license": "MIT", "dependencies": { "mlly": "^1.7.4", @@ -5415,14 +5484,12 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "dev": true, "license": "MIT" }, "node_modules/local-pkg/node_modules/pkg-types": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", - "dev": true, "license": "MIT", "dependencies": { "confbox": "^0.2.4", @@ -5486,6 +5553,21 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magic-string-ast": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz", + "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.19" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5578,7 +5660,6 @@ "version": "1.8.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", - "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.16.0", @@ -5598,7 +5679,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -5678,6 +5758,12 @@ "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", "license": "BSD-3-Clause" }, + "node_modules/nostics": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz", + "integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==", + "license": "MIT" + }, "node_modules/npm-run-all": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", @@ -6155,7 +6241,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, "license": "MIT" }, "node_modules/perfect-debounce": { @@ -6230,7 +6315,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, "license": "MIT", "dependencies": { "confbox": "^0.1.8", @@ -6301,9 +6385,9 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", "bin": { @@ -6350,7 +6434,6 @@ "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", - "dev": true, "funding": [ { "type": "individual", @@ -6411,13 +6494,12 @@ } }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "license": "MIT", "engines": { - "node": ">= 14.18.0" + "node": ">= 20.19.0" }, "funding": { "type": "individual", @@ -6536,7 +6618,7 @@ "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -6657,13 +6739,13 @@ } }, "node_modules/sass": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", - "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", + "version": "1.102.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.102.0.tgz", + "integrity": "sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": "^4.0.0", + "chokidar": "^5.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, @@ -6671,7 +6753,7 @@ "sass": "sass.js" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.19.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" @@ -6704,7 +6786,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", - "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -7293,9 +7374,9 @@ } }, "node_modules/terser": { - "version": "5.46.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", - "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", + "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7332,7 +7413,6 @@ "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -7349,7 +7429,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -7367,7 +7446,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "engines": { "node": ">=12" }, @@ -7590,7 +7668,6 @@ "version": "1.6.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", - "dev": true, "license": "MIT" }, "node_modules/unbox-primitive": { @@ -7890,7 +7967,6 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz", "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==", - "dev": true, "license": "MIT", "dependencies": { "pathe": "^2.0.3", @@ -7907,7 +7983,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7949,22 +8024,6 @@ } } }, - "node_modules/unplugin-vue-components/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/unplugin-vue-components/node_modules/picomatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", @@ -7978,20 +8037,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/unplugin-vue-components/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/unplugin-vue-components/node_modules/unplugin": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz", @@ -8109,7 +8154,7 @@ "version": "7.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", - "dev": true, + "devOptional": true, "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", @@ -8197,7 +8242,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -8215,7 +8260,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=12" }, @@ -8334,16 +8379,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", - "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-sfc": "3.5.30", - "@vue/runtime-dom": "3.5.30", - "@vue/server-renderer": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" }, "peerDependencies": { "typescript": "*" @@ -8358,7 +8403,6 @@ "version": "3.3.8", "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.8.tgz", "integrity": "sha512-troqCMmQodQDqUqn63NQaFi+CDSclSe7sc8VEBFqf5GFLqmGR2Ph3P2WEC7qwpRVyEWsTi/aAr4vyOe/B1hU3g==", - "dev": true, "license": "MIT" }, "node_modules/vue-eslint-parser": { @@ -8399,35 +8443,163 @@ } }, "node_modules/vue-router": { - "version": "4.6.4", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", - "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.2.0.tgz", + "integrity": "sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==", "license": "MIT", "dependencies": { - "@vue/devtools-api": "^6.6.4" + "@babel/generator": "^8.0.0", + "@vue-macros/common": "^3.1.3", + "@vue/devtools-api": "^8.1.5", + "ast-walker-scope": "^0.9.0", + "chokidar": "^5.0.0", + "json5": "^2.2.3", + "local-pkg": "^1.2.1", + "magic-string": "^0.30.21", + "mlly": "^1.8.2", + "muggle-string": "^0.4.1", + "nostics": "^1.1.4", + "pathe": "^2.0.3", + "picomatch": "^4.0.5", + "scule": "^1.3.0", + "tinyglobby": "^0.2.17", + "unplugin": "^3.3.0", + "unplugin-utils": "^0.3.2", + "yaml": "^2.9.0" }, "funding": { "url": "https://github.com/sponsors/posva" }, "peerDependencies": { - "vue": "^3.5.0" + "@pinia/colada": ">=0.21.2", + "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", + "pinia": "^3.0.4 || ^4.0.2", + "vite": "^7.3.0 || ^8.0.0", + "vue": "^3.5.34 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "pinia": { + "optional": true + }, + "vite": { + "optional": true + } } }, "node_modules/vue-router/node_modules/@vue/devtools-api": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", - "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz", + "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.2.1" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-kit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz", + "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.2.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-shared": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz", + "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==", "license": "MIT" }, + "node_modules/vue-router/node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/vue-router/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vue-router/node_modules/unplugin": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz", + "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.4", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, "node_modules/vue-tsc": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz", - "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.8.tgz", + "integrity": "sha512-xXmYlVQpcwJDWyGlqbHrGVOl1h3UOsASymRibrHc+iy9j/UNnOrOn4u+fntHz4D6Cs74RtapeqVV6CzJeg+UlA==", "dev": true, "license": "MIT", "dependencies": { "@volar/typescript": "2.4.28", - "@vue/language-core": "3.2.6" + "@vue/language-core": "3.3.8" }, "bin": { "vue-tsc": "bin/vue-tsc.js" @@ -8463,7 +8635,6 @@ "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "dev": true, "license": "MIT" }, "node_modules/whatwg-mimetype": { @@ -8735,6 +8906,21 @@ "dev": true, "license": "MIT" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/web/package.json b/web/package.json index 78a8461e..a7229979 100644 --- a/web/package.json +++ b/web/package.json @@ -9,6 +9,7 @@ "devDependencies": { "@vue/test-utils": "2.4.11", "jsdom": "29.1.1", - "vitest": "4.1.10" + "vitest": "4.1.10", + "vue": "^3.5.40" } } From effa49685950c563243eea8ed3acc7c4e4bbe4c7 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 30 Jul 2026 13:19:55 +0800 Subject: [PATCH 09/23] feat: add negotiated binary UDP packets (#5456) --- client/control.go | 11 +- client/control_session.go | 13 +- client/control_session_test.go | 2 + client/control_udp_test.go | 125 +++++++ client/proxy/proxy.go | 9 +- client/proxy/proxy_manager.go | 7 +- client/proxy/proxy_wrapper.go | 3 +- client/proxy/udp.go | 13 +- client/service_shutdown_test.go | 2 +- pkg/msg/udp_binary.go | 338 +++++++++++++++++++ pkg/msg/udp_binary_test.go | 248 ++++++++++++++ pkg/msg/wire_v2.go | 1 + pkg/msg/wire_v2_test.go | 3 + pkg/proto/wire/crypto.go | 19 +- pkg/proto/wire/wire.go | 10 +- pkg/proto/wire/wire_test.go | 30 ++ server/control.go | 4 +- server/proxy/proxy.go | 57 ++-- server/proxy/udp.go | 8 +- server/service.go | 66 +++- server/service_test.go | 201 ++++++++++- test/e2e/compatibility/compatibility_test.go | 51 +++ test/e2e/v1/basic/wire.go | 82 +++++ 23 files changed, 1232 insertions(+), 71 deletions(-) create mode 100644 client/control_udp_test.go create mode 100644 pkg/msg/udp_binary.go create mode 100644 pkg/msg/udp_binary_test.go diff --git a/client/control.go b/client/control.go index 6e3002bc..5980ed34 100644 --- a/client/control.go +++ b/client/control.go @@ -47,6 +47,8 @@ type SessionContext struct { Connector MessageConnector // Virtual net controller VnetController *vnet.Controller + // UDPPacketCodec is immutable for the lifetime of this negotiated session. + UDPPacketCodec string } type Control struct { @@ -92,7 +94,14 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro ctl.registerMsgHandlers() ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher) - ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, sessionCtx.Auth.EncryptionKey(), ctl.msgTransporter, sessionCtx.VnetController) + ctl.pm = proxy.NewManager( + ctl.ctx, + sessionCtx.Common, + sessionCtx.Auth.EncryptionKey(), + ctl.msgTransporter, + sessionCtx.VnetController, + sessionCtx.UDPPacketCodec, + ) ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common, ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController) return ctl, nil diff --git a/client/control_session.go b/client/control_session.go index d533ba2d..e3ed27fc 100644 --- a/client/control_session.go +++ b/client/control_session.go @@ -99,6 +99,7 @@ func (d *controlSessionDialer) Dial(previousRunID string) (*SessionContext, erro Auth: d.auth, Connector: newMessageConnector(connector, d.common.Transport.WireProtocol), VnetController: d.vnetController, + UDPPacketCodec: loginResult.udpPacketCodec, }, nil } @@ -127,8 +128,9 @@ func (d *controlSessionDialer) buildLoginMsg(previousRunID string) (*msg.Login, } type loginExchangeResult struct { - resp *msg.LoginResp - crypto *wire.CryptoContext + resp *msg.LoginResp + crypto *wire.CryptoContext + udpPacketCodec string } func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) (*loginExchangeResult, error) { @@ -172,6 +174,7 @@ func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) }() var cryptoContext *wire.CryptoContext + var udpPacketCodec string if wireConn != nil { serverHelloFrame, err := wireConn.ReadFrame() if err != nil { @@ -191,6 +194,7 @@ func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) if err != nil { return nil, err } + udpPacketCodec = serverHello.Selected.Message.UDPPacketCodec } var loginRespMsg msg.LoginResp @@ -198,8 +202,9 @@ func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) return nil, err } return &loginExchangeResult{ - resp: &loginRespMsg, - crypto: cryptoContext, + resp: &loginRespMsg, + crypto: cryptoContext, + udpPacketCodec: udpPacketCodec, }, nil } diff --git a/client/control_session_test.go b/client/control_session_test.go index a0778fba..4bbcd453 100644 --- a/client/control_session_test.go +++ b/client/control_session_test.go @@ -117,6 +117,7 @@ func TestControlSessionDialerDialV1(t *testing.T) { defer sessionCtx.Connector.Close() require.Equal(t, "run-v1", sessionCtx.RunID) + require.Empty(t, sessionCtx.UDPPacketCodec) require.NotNil(t, sessionCtx.Conn) require.NotNil(t, sessionCtx.Connector) require.False(t, connector.closed.Load()) @@ -225,6 +226,7 @@ func TestControlSessionDialerDialV2(t *testing.T) { defer sessionCtx.Connector.Close() require.Equal(t, "run-v2", sessionCtx.RunID) + require.Equal(t, wire.UDPPacketCodecBinary, sessionCtx.UDPPacketCodec) require.NotNil(t, sessionCtx.Conn) require.NotNil(t, sessionCtx.Connector) require.False(t, connector.closed.Load()) diff --git a/client/control_udp_test.go b/client/control_udp_test.go new file mode 100644 index 00000000..f3c91232 --- /dev/null +++ b/client/control_udp_test.go @@ -0,0 +1,125 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !frps + +package client + +import ( + "context" + "encoding/binary" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + + clientproxy "github.com/fatedier/frp/client/proxy" + "github.com/fatedier/frp/pkg/auth" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/proto/wire" +) + +func TestControlPropagatesBinaryUDPPacketCodecToWorkConn(t *testing.T) { + echoConn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")}) + require.NoError(t, err) + t.Cleanup(func() { _ = echoConn.Close() }) + + echoDone := make(chan error, 1) + go func() { + buf := make([]byte, 64) + n, addr, err := echoConn.ReadFromUDP(buf) + if err == nil { + _, err = echoConn.WriteToUDP(buf[:n], addr) + } + echoDone <- err + }() + + authRuntime, err := auth.BuildClientAuth(&v1.AuthClientConfig{ + Method: v1.AuthMethodToken, + Token: "token", + }) + require.NoError(t, err) + + controlConn, controlPeer := net.Pipe() + t.Cleanup(func() { + _ = controlConn.Close() + _ = controlPeer.Close() + }) + common := &v1.ClientCommonConfig{ + Transport: v1.ClientTransportConfig{WireProtocol: wire.ProtocolV2}, + UDPPacketSize: 1500, + } + ctl, err := NewControl(context.Background(), &SessionContext{ + Common: common, + RunID: "binary-udp-test", + Conn: msg.NewConn(controlConn, msg.NewV2ReadWriter(controlConn)), + Auth: authRuntime, + UDPPacketCodec: wire.UDPPacketCodecBinary, + }) + require.NoError(t, err) + t.Cleanup(ctl.pm.Close) + + echoAddr := echoConn.LocalAddr().(*net.UDPAddr) + proxyCfg := &v1.UDPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: "udp", + Type: string(v1.ProxyTypeUDP), + ProxyBackend: v1.ProxyBackend{ + LocalIP: "127.0.0.1", + LocalPort: echoAddr.Port, + }, + }, + } + ctl.pm.UpdateAll([]v1.ProxyConfigurer{proxyCfg}) + require.Eventually(t, func() bool { + status, ok := ctl.pm.GetProxyStatus("udp") + return ok && status.Phase == clientproxy.ProxyPhaseWaitStart + }, time.Second, 10*time.Millisecond) + require.NoError(t, ctl.pm.StartProxy("udp", "", "")) + + workClient, workServer := net.Pipe() + t.Cleanup(func() { + _ = workClient.Close() + _ = workServer.Close() + }) + deadline := time.Now().Add(3 * time.Second) + require.NoError(t, workClient.SetDeadline(deadline)) + require.NoError(t, workServer.SetDeadline(deadline)) + ctl.pm.HandleWorkConn("udp", workClient, &msg.StartWorkConn{ProxyName: "udp"}) + + serverRW, err := msg.NewUDPPacketReadWriter(workServer, wire.ProtocolV2, wire.UDPPacketCodecBinary) + require.NoError(t, err) + writeDone := make(chan error, 1) + in := &msg.UDPPacket{ + Content: []byte("binary udp"), + RemoteAddr: &net.UDPAddr{IP: net.ParseIP("192.0.2.1"), Port: 12345}, + } + go func() { + writeDone <- serverRW.WriteMsg(in) + }() + + frame, err := wire.NewConn(workServer).ReadFrame() + require.NoError(t, err) + require.Equal(t, wire.FrameTypeMessage, frame.Type) + require.GreaterOrEqual(t, len(frame.Payload), 2) + require.Equal(t, msg.V2TypeUDPPacketBinary, binary.BigEndian.Uint16(frame.Payload[:2])) + out, err := msg.DecodeUDPPacketBinary(frame.Payload[2:]) + require.NoError(t, err) + require.Equal(t, in.Content, out.Content) + require.Equal(t, in.RemoteAddr.String(), out.RemoteAddr.String()) + require.NoError(t, <-writeDone) + require.NoError(t, <-echoDone) +} diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index 84ff49a1..5af56599 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -61,6 +61,7 @@ func NewProxy( encryptionKey []byte, msgTransporter transport.MessageTransporter, vnetController *vnet.Controller, + udpPacketCodec string, ) (pxy Proxy) { var limiter *rate.Limiter limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes() @@ -77,6 +78,7 @@ func NewProxy( vnetController: vnetController, xl: xlog.FromContextSafe(ctx), ctx: ctx, + udpPacketCodec: udpPacketCodec, } factory := proxyFactoryRegistry[reflect.TypeOf(pxyConf)] @@ -98,9 +100,10 @@ type BaseProxy struct { proxyPlugin plugin.Plugin inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool - mu sync.RWMutex - xl *xlog.Logger - ctx context.Context + mu sync.RWMutex + xl *xlog.Logger + ctx context.Context + udpPacketCodec string } func (pxy *BaseProxy) Run() error { diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go index 42f9f589..6bf8d4a8 100644 --- a/client/proxy/proxy_manager.go +++ b/client/proxy/proxy_manager.go @@ -43,7 +43,8 @@ type Manager struct { encryptionKey []byte clientCfg *v1.ClientCommonConfig - ctx context.Context + ctx context.Context + udpPacketCodec string } func NewManager( @@ -52,6 +53,7 @@ func NewManager( encryptionKey []byte, msgTransporter transport.MessageTransporter, vnetController *vnet.Controller, + udpPacketCodec string, ) *Manager { return &Manager{ proxies: make(map[string]*Wrapper), @@ -61,6 +63,7 @@ func NewManager( encryptionKey: encryptionKey, clientCfg: clientCfg, ctx: ctx, + udpPacketCodec: udpPacketCodec, } } @@ -166,7 +169,7 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) { for _, cfg := range proxyCfgs { name := cfg.GetBaseConfig().Name if _, ok := pm.proxies[name]; !ok { - pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.encryptionKey, pm.HandleEvent, pm.msgTransporter, pm.vnetController) + pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.encryptionKey, pm.HandleEvent, pm.msgTransporter, pm.vnetController, pm.udpPacketCodec) if pm.inWorkConnCallback != nil { pxy.SetInWorkConnCallback(pm.inWorkConnCallback) } diff --git a/client/proxy/proxy_wrapper.go b/client/proxy/proxy_wrapper.go index 718c02e6..37a71277 100644 --- a/client/proxy/proxy_wrapper.go +++ b/client/proxy/proxy_wrapper.go @@ -99,6 +99,7 @@ func NewWrapper( eventHandler event.Handler, msgTransporter transport.MessageTransporter, vnetController *vnet.Controller, + udpPacketCodec string, ) *Wrapper { baseInfo := cfg.GetBaseConfig() xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name) @@ -127,7 +128,7 @@ func NewWrapper( xl.Tracef("enable health check monitor") } - pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, encryptionKey, pw.msgTransporter, pw.vnetController) + pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, encryptionKey, pw.msgTransporter, pw.vnetController, udpPacketCodec) return pw } diff --git a/client/proxy/udp.go b/client/proxy/udp.go index 110c01ac..d01d2c8e 100644 --- a/client/proxy/udp.go +++ b/client/proxy/udp.go @@ -97,10 +97,17 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { return } - pxy.mu.Lock() - pxy.workConn = netpkg.WrapReadWriteCloserToConn(remote, conn) + workConn := netpkg.WrapReadWriteCloserToConn(remote, conn) // Plain UDP payload follows the configured wire protocol for message framing. - payloadRW := msg.NewReadWriter(pxy.workConn, pxy.clientCfg.Transport.WireProtocol) + payloadRW, err := msg.NewUDPPacketReadWriter(workConn, pxy.clientCfg.Transport.WireProtocol, pxy.udpPacketCodec) + if err != nil { + xl.Errorf("create UDP packet read writer: %v", err) + workConn.Close() + return + } + + pxy.mu.Lock() + pxy.workConn = workConn pxy.readCh = make(chan *msg.UDPPacket, 1024) pxy.sendCh = make(chan msg.Message, 1024) pxy.closed = false diff --git a/client/service_shutdown_test.go b/client/service_shutdown_test.go index 5c710bec..3b956726 100644 --- a/client/service_shutdown_test.go +++ b/client/service_shutdown_test.go @@ -34,7 +34,7 @@ func newGracefulCloseTestService() *Service { }, doneCh: make(chan struct{}), } - ctl.pm = proxy.NewManager(ctx, common, nil, nil, nil) + ctl.pm = proxy.NewManager(ctx, common, nil, nil, nil, "") ctl.vm = visitor.NewManager(ctx, "graceful-close-race", common, nil, nil, nil) return &Service{ctl: ctl, cancel: context.CancelCauseFunc(func(error) {})} } diff --git a/pkg/msg/udp_binary.go b/pkg/msg/udp_binary.go new file mode 100644 index 00000000..7493c70f --- /dev/null +++ b/pkg/msg/udp_binary.go @@ -0,0 +1,338 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package msg + +import ( + "encoding/binary" + "fmt" + "io" + "net" + "unicode/utf8" + + "github.com/fatedier/frp/pkg/proto/wire" +) + +const MaxUDPPayloadSize = 65507 + +const ( + udpPacketFlagLocalAddr byte = 1 << 0 + udpPacketFlagRemoteAddr byte = 1 << 1 + udpPacketValidFlags = udpPacketFlagLocalAddr | udpPacketFlagRemoteAddr +) + +type binaryUDPAddr struct { + family byte + ip []byte + port uint16 + zone string +} + +// EncodeUDPPacketBinary encodes the body of a V2 binary UDP packet message. +// RemoteAddr is required by the UDP forwarding path. +func EncodeUDPPacketBinary(packet *UDPPacket) ([]byte, error) { + if packet == nil { + return nil, fmt.Errorf("nil UDP packet") + } + if packet.RemoteAddr == nil { + return nil, fmt.Errorf("UDP packet missing remote address") + } + if len(packet.Content) > MaxUDPPayloadSize { + return nil, fmt.Errorf("UDP payload length %d exceeds limit %d", len(packet.Content), MaxUDPPayloadSize) + } + + var flags byte + var localAddr, remoteAddr binaryUDPAddr + bodyLen := 1 + 2 + len(packet.Content) + if packet.LocalAddr != nil { + flags |= udpPacketFlagLocalAddr + var err error + localAddr, err = validateBinaryUDPAddr(packet.LocalAddr) + if err != nil { + return nil, fmt.Errorf("local address: %w", err) + } + bodyLen += binaryUDPAddrLen(localAddr) + } + flags |= udpPacketFlagRemoteAddr + var err error + remoteAddr, err = validateBinaryUDPAddr(packet.RemoteAddr) + if err != nil { + return nil, fmt.Errorf("remote address: %w", err) + } + bodyLen += binaryUDPAddrLen(remoteAddr) + if 2+bodyLen > wire.DefaultMaxFramePayloadSize { + return nil, fmt.Errorf("v2 frame payload length %d exceeds limit %d", 2+bodyLen, wire.DefaultMaxFramePayloadSize) + } + + body := make([]byte, bodyLen) + body[0] = flags + offset := 1 + if flags&udpPacketFlagLocalAddr != 0 { + offset = putBinaryUDPAddr(body, offset, localAddr) + } + offset = putBinaryUDPAddr(body, offset, remoteAddr) + binary.BigEndian.PutUint16(body[offset:offset+2], uint16(len(packet.Content))) + offset += 2 + copy(body[offset:], packet.Content) + return body, nil +} + +// DecodeUDPPacketBinary decodes a V2 binary UDP packet body and returns data +// that does not alias the input frame buffer. +func DecodeUDPPacketBinary(body []byte) (*UDPPacket, error) { + if len(body) < 3 { + return nil, fmt.Errorf("UDP packet body too short: %d", len(body)) + } + if 2+len(body) > wire.DefaultMaxFramePayloadSize { + return nil, fmt.Errorf("v2 frame payload length %d exceeds limit %d", 2+len(body), wire.DefaultMaxFramePayloadSize) + } + + flags := body[0] + if flags&^udpPacketValidFlags != 0 { + return nil, fmt.Errorf("reserved UDP packet flags set: 0x%02x", flags) + } + if flags&udpPacketFlagRemoteAddr == 0 { + return nil, fmt.Errorf("UDP packet missing remote address") + } + + packet := &UDPPacket{} + offset := 1 + var err error + if flags&udpPacketFlagLocalAddr != 0 { + packet.LocalAddr, offset, err = readBinaryUDPAddr(body, offset) + if err != nil { + return nil, fmt.Errorf("local address: %w", err) + } + } + if flags&udpPacketFlagRemoteAddr != 0 { + packet.RemoteAddr, offset, err = readBinaryUDPAddr(body, offset) + if err != nil { + return nil, fmt.Errorf("remote address: %w", err) + } + } + if len(body)-offset < 2 { + return nil, fmt.Errorf("truncated UDP payload length") + } + payloadLen := int(binary.BigEndian.Uint16(body[offset : offset+2])) + offset += 2 + if payloadLen > MaxUDPPayloadSize { + return nil, fmt.Errorf("UDP payload length %d exceeds limit %d", payloadLen, MaxUDPPayloadSize) + } + remaining := len(body) - offset + if remaining < payloadLen { + return nil, fmt.Errorf("truncated UDP payload: have %d want %d", remaining, payloadLen) + } + if remaining > payloadLen { + return nil, fmt.Errorf("trailing UDP packet bytes: %d", remaining-payloadLen) + } + packet.Content = append([]byte(nil), body[offset:offset+payloadLen]...) + return packet, nil +} + +func validateBinaryUDPAddr(addr *net.UDPAddr) (binaryUDPAddr, error) { + if addr.Port < 0 || addr.Port > 65535 { + return binaryUDPAddr{}, fmt.Errorf("port out of range: %d", addr.Port) + } + if ip := addr.IP.To4(); ip != nil { + if addr.Zone != "" { + return binaryUDPAddr{}, fmt.Errorf("IPv4 zone is forbidden") + } + return binaryUDPAddr{family: 4, ip: ip, port: uint16(addr.Port)}, nil + } + ip := addr.IP.To16() + if ip == nil { + return binaryUDPAddr{}, fmt.Errorf("invalid IP") + } + if len(addr.Zone) > 255 { + return binaryUDPAddr{}, fmt.Errorf("zone exceeds 255 bytes") + } + if !utf8.ValidString(addr.Zone) { + return binaryUDPAddr{}, fmt.Errorf("zone is not valid UTF-8") + } + return binaryUDPAddr{family: 6, ip: ip, port: uint16(addr.Port), zone: addr.Zone}, nil +} + +func binaryUDPAddrLen(addr binaryUDPAddr) int { + return 1 + len(addr.ip) + 2 + 1 + len(addr.zone) +} + +func putBinaryUDPAddr(body []byte, offset int, addr binaryUDPAddr) int { + body[offset] = addr.family + offset++ + copy(body[offset:], addr.ip) + offset += len(addr.ip) + binary.BigEndian.PutUint16(body[offset:offset+2], addr.port) + offset += 2 + body[offset] = byte(len(addr.zone)) + offset++ + copy(body[offset:], addr.zone) + return offset + len(addr.zone) +} + +func readBinaryUDPAddr(body []byte, offset int) (*net.UDPAddr, int, error) { + if offset >= len(body) { + return nil, offset, fmt.Errorf("truncated address family") + } + family := body[offset] + offset++ + var ipLen int + switch family { + case 4: + ipLen = net.IPv4len + case 6: + ipLen = net.IPv6len + default: + return nil, offset, fmt.Errorf("unknown address family %d", family) + } + if len(body)-offset < ipLen+3 { + return nil, offset, fmt.Errorf("truncated address") + } + ip := append(net.IP(nil), body[offset:offset+ipLen]...) + offset += ipLen + port := binary.BigEndian.Uint16(body[offset : offset+2]) + offset += 2 + zoneLen := int(body[offset]) + offset++ + if len(body)-offset < zoneLen { + return nil, offset, fmt.Errorf("truncated zone") + } + zoneBytes := body[offset : offset+zoneLen] + if family == 4 && zoneLen != 0 { + return nil, offset, fmt.Errorf("IPv4 zone is forbidden") + } + if !utf8.Valid(zoneBytes) { + return nil, offset, fmt.Errorf("zone is not valid UTF-8") + } + offset += zoneLen + return &net.UDPAddr{IP: ip, Port: int(port), Zone: string(zoneBytes)}, offset, nil +} + +type V2BinaryUDPPacketReadWriter struct { + conn *wire.Conn +} + +func NewV2BinaryUDPPacketReadWriter(rw io.ReadWriter) *V2BinaryUDPPacketReadWriter { + return &V2BinaryUDPPacketReadWriter{conn: wire.NewConn(rw)} +} + +func (rw *V2BinaryUDPPacketReadWriter) ReadMsg() (Message, error) { + frame, err := rw.conn.ReadFrame() + if err != nil { + return nil, err + } + if isV2MessageType(frame, V2TypeUDPPacketBinary) { + return decodeV2BinaryUDPPacketFrame(frame) + } + if isV2MessageType(frame, V2TypeUDPPacket) { + return nil, fmt.Errorf("received JSON UDP packet after binary codec negotiation") + } + return DecodeV2MessageFrame(frame) +} + +func (rw *V2BinaryUDPPacketReadWriter) ReadMsgInto(out Message) error { + frame, err := rw.conn.ReadFrame() + if err != nil { + return err + } + if packetOut, ok := out.(*UDPPacket); ok { + if !isV2MessageType(frame, V2TypeUDPPacketBinary) { + return unexpectedV2UDPPacketType(frame) + } + packet, err := decodeV2BinaryUDPPacketFrame(frame) + if err != nil { + return err + } + *packetOut = *packet + return nil + } + return DecodeV2MessageFrameInto(frame, out) +} + +func (rw *V2BinaryUDPPacketReadWriter) WriteMsg(message Message) error { + var packet *UDPPacket + switch typed := message.(type) { + case *UDPPacket: + packet = typed + case UDPPacket: + packet = &typed + default: + frame, err := EncodeV2MessageFrame(message) + if err != nil { + return err + } + return rw.conn.WriteFrame(frame) + } + body, err := EncodeUDPPacketBinary(packet) + if err != nil { + return err + } + payload := make([]byte, 2+len(body)) + binary.BigEndian.PutUint16(payload[:2], V2TypeUDPPacketBinary) + copy(payload[2:], body) + return rw.conn.WriteFrame(&wire.Frame{Type: wire.FrameTypeMessage, Payload: payload}) +} + +func decodeV2BinaryUDPPacketFrame(frame *wire.Frame) (*UDPPacket, error) { + if frame.Type != wire.FrameTypeMessage { + return nil, fmt.Errorf("unexpected frame type %d, want %d", frame.Type, wire.FrameTypeMessage) + } + if len(frame.Payload) < 2 { + return nil, fmt.Errorf("message frame payload too short") + } + if binary.BigEndian.Uint16(frame.Payload[:2]) != V2TypeUDPPacketBinary { + return nil, unexpectedV2UDPPacketType(frame) + } + return DecodeUDPPacketBinary(frame.Payload[2:]) +} + +func isV2MessageType(frame *wire.Frame, typeID uint16) bool { + return frame.Type == wire.FrameTypeMessage && len(frame.Payload) >= 2 && binary.BigEndian.Uint16(frame.Payload[:2]) == typeID +} + +func unexpectedV2UDPPacketType(frame *wire.Frame) error { + if frame.Type != wire.FrameTypeMessage { + return fmt.Errorf("unexpected frame type %d, want %d", frame.Type, wire.FrameTypeMessage) + } + if len(frame.Payload) < 2 { + return fmt.Errorf("message frame payload too short") + } + typeID := binary.BigEndian.Uint16(frame.Payload[:2]) + if typeID == V2TypeUDPPacket { + return fmt.Errorf("received JSON UDP packet after binary codec negotiation") + } + return fmt.Errorf("unexpected message type %d, want %d", typeID, V2TypeUDPPacketBinary) +} + +// NewUDPPacketReadWriter selects the negotiated packet codec without changing +// the framing or codecs used by non-UDP messages on the work connection. +func NewUDPPacketReadWriter(rw io.ReadWriter, wireProtocol, udpPacketCodec string) (ReadWriter, error) { + switch wireProtocol { + case "", wire.ProtocolV1: + if udpPacketCodec != "" { + return nil, fmt.Errorf("UDP packet codec %q requires wire protocol v2", udpPacketCodec) + } + return NewV1ReadWriter(rw), nil + case wire.ProtocolV2: + switch udpPacketCodec { + case "": + return NewV2ReadWriter(rw), nil + case wire.UDPPacketCodecBinary: + return NewV2BinaryUDPPacketReadWriter(rw), nil + default: + return nil, fmt.Errorf("unsupported UDP packet codec %q", udpPacketCodec) + } + default: + return nil, fmt.Errorf("unsupported wire protocol %q", wireProtocol) + } +} diff --git a/pkg/msg/udp_binary_test.go b/pkg/msg/udp_binary_test.go new file mode 100644 index 00000000..7dac31fb --- /dev/null +++ b/pkg/msg/udp_binary_test.go @@ -0,0 +1,248 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package msg + +import ( + "bytes" + "encoding/binary" + "net" + "strconv" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/proto/wire" +) + +func TestUDPPacketBinaryRoundTrip(t *testing.T) { + payload := bytes.Repeat([]byte{0xa5}, 1472) + in := &UDPPacket{ + Content: payload, + LocalAddr: &net.UDPAddr{ + IP: net.ParseIP("2001:db8::1"), + Port: 1234, + Zone: "en0", + }, + RemoteAddr: &net.UDPAddr{IP: net.ParseIP("203.0.113.9"), Port: 54321}, + } + body, err := EncodeUDPPacketBinary(in) + require.NoError(t, err) + out, err := DecodeUDPPacketBinary(body) + require.NoError(t, err) + require.Equal(t, in.Content, out.Content) + require.Equal(t, in.LocalAddr.String(), out.LocalAddr.String()) + require.Equal(t, in.RemoteAddr.String(), out.RemoteAddr.String()) + body[len(body)-1] ^= 0xff + body[25] ^= 0xff + require.Equal(t, byte(0xa5), out.Content[len(out.Content)-1], "decoded payload must own frame bytes") + require.Equal(t, byte(203), out.RemoteAddr.IP.To4()[0], "decoded address must own frame bytes") +} + +func TestUDPPacketBinarySizesAndOptionalLocalAddress(t *testing.T) { + for _, size := range []int{0, 32, 128, 512, 1200, 1472, 4096, 49107, 65507} { + t.Run(strconv.Itoa(size), func(t *testing.T) { + in := &UDPPacket{ + Content: bytes.Repeat([]byte{byte(size)}, size), + RemoteAddr: &net.UDPAddr{IP: net.ParseIP("203.0.113.9"), Port: 54321}, + } + body, err := EncodeUDPPacketBinary(in) + require.NoError(t, err) + out, err := DecodeUDPPacketBinary(body) + require.NoError(t, err) + require.Equal(t, len(in.Content), len(out.Content)) + if size == 0 { + require.Empty(t, out.Content) + } else { + require.Equal(t, in.Content, out.Content) + } + }) + } +} + +func TestUDPPacketBinaryMalformed(t *testing.T) { + valid, err := EncodeUDPPacketBinary(&UDPPacket{ + Content: []byte("payload"), + RemoteAddr: &net.UDPAddr{IP: net.ParseIP("203.0.113.9"), Port: 54321}, + }) + require.NoError(t, err) + tests := [][]byte{ + {0x80, 0, 0}, + {0x02, 4, 1, 2}, + {0x02, 4, 1, 2, 3, 4, 0xd4}, + append(append([]byte(nil), valid...), 0), + } + for _, malformed := range tests { + _, err := DecodeUDPPacketBinary(malformed) + require.Error(t, err) + } + _, err = DecodeUDPPacketBinary([]byte{0, 0, 0}) + require.ErrorContains(t, err, "missing remote address") + payloadLengthOffset := len(valid) - len("payload") - 2 + invalidPayloadLength := append([]byte(nil), valid...) + binary.BigEndian.PutUint16(invalidPayloadLength[payloadLengthOffset:payloadLengthOffset+2], 0xffff) + _, err = DecodeUDPPacketBinary(invalidPayloadLength) + require.ErrorContains(t, err, "payload length") + truncatedPayload := append([]byte(nil), valid[:payloadLengthOffset+2]...) + binary.BigEndian.PutUint16(truncatedPayload[payloadLengthOffset:payloadLengthOffset+2], 1) + _, err = DecodeUDPPacketBinary(truncatedPayload) + require.ErrorContains(t, err, "truncated UDP payload") + _, err = DecodeUDPPacketBinary(make([]byte, wire.DefaultMaxFramePayloadSize)) + require.ErrorContains(t, err, "frame payload length") + + badIPv4Zone := []byte{2, 4, 203, 0, 113, 9, 0xd4, 0x31, 1, 'z', 0, 0} + _, err = DecodeUDPPacketBinary(badIPv4Zone) + require.ErrorContains(t, err, "IPv4 zone") + badFamily := []byte{2, 9, 0, 0} + _, err = DecodeUDPPacketBinary(badFamily) + require.ErrorContains(t, err, "unknown address family") + badUTF8 := make([]byte, 0, 24) + badUTF8 = append(badUTF8, 2, 6) + badUTF8 = append(badUTF8, make([]byte, 16)...) + badUTF8 = append(badUTF8, 0, 1, 1, 0xff, 0, 0) + _, err = DecodeUDPPacketBinary(badUTF8) + require.ErrorContains(t, err, "UTF-8") +} + +func TestUDPPacketBinaryEncodeRejectsInvalidPackets(t *testing.T) { + _, err := EncodeUDPPacketBinary(&UDPPacket{}) + require.ErrorContains(t, err, "missing remote address") + _, err = EncodeUDPPacketBinary(&UDPPacket{ + LocalAddr: &net.UDPAddr{IP: net.ParseIP("192.0.2.1"), Port: 1234}, + }) + require.ErrorContains(t, err, "missing remote address") + _, err = EncodeUDPPacketBinary(&UDPPacket{ + Content: make([]byte, MaxUDPPayloadSize+1), + RemoteAddr: &net.UDPAddr{IP: net.ParseIP("203.0.113.9"), Port: 54321}, + }) + require.ErrorContains(t, err, "exceeds limit") + _, err = EncodeUDPPacketBinary(&UDPPacket{RemoteAddr: &net.UDPAddr{IP: net.ParseIP("203.0.113.9"), Port: 1, Zone: "bad"}}) + require.ErrorContains(t, err, "IPv4 zone") + _, err = EncodeUDPPacketBinary(&UDPPacket{RemoteAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 1, Zone: string(bytes.Repeat([]byte{'z'}, 256))}}) + require.ErrorContains(t, err, "zone exceeds") + _, err = EncodeUDPPacketBinary(&UDPPacket{RemoteAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 1, Zone: string([]byte{0xff})}}) + require.ErrorContains(t, err, "UTF-8") + _, err = EncodeUDPPacketBinary(&UDPPacket{RemoteAddr: &net.UDPAddr{Port: -1}}) + require.ErrorContains(t, err, "port out of range") + _, err = EncodeUDPPacketBinary(&UDPPacket{RemoteAddr: &net.UDPAddr{Port: 65536}}) + require.ErrorContains(t, err, "port out of range") + _, err = EncodeUDPPacketBinary(&UDPPacket{RemoteAddr: &net.UDPAddr{IP: net.IP{1, 2, 3}}}) + require.ErrorContains(t, err, "invalid IP") + _, err = EncodeUDPPacketBinary(&UDPPacket{ + Content: make([]byte, MaxUDPPayloadSize), + RemoteAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Zone: string(bytes.Repeat([]byte{'z'}, 255))}, + }) + require.ErrorContains(t, err, "frame payload length") +} + +func TestV2BinaryUDPPacketReadWriterPreservesOtherMessages(t *testing.T) { + var buf bytes.Buffer + rw := NewV2BinaryUDPPacketReadWriter(&buf) + in := &UDPPacket{Content: []byte("udp"), RemoteAddr: &net.UDPAddr{IP: net.ParseIP("203.0.113.9"), Port: 54321}} + require.NoError(t, rw.WriteMsg(in)) + require.NoError(t, rw.WriteMsg(&Ping{Timestamp: 7})) + frameConn := wire.NewConn(&buf) + frame, err := frameConn.ReadFrame() + require.NoError(t, err) + require.Equal(t, V2TypeUDPPacketBinary, binary.BigEndian.Uint16(frame.Payload[:2])) + frame, err = frameConn.ReadFrame() + require.NoError(t, err) + require.Equal(t, V2TypePing, binary.BigEndian.Uint16(frame.Payload[:2])) +} + +func TestV2BinaryUDPPacketReadWriterRoundTripAndCodecInvariant(t *testing.T) { + in := &UDPPacket{Content: []byte("udp"), RemoteAddr: &net.UDPAddr{IP: net.ParseIP("203.0.113.9"), Port: 54321}} + var binaryStream bytes.Buffer + binaryWriter, err := NewUDPPacketReadWriter(&binaryStream, wire.ProtocolV2, wire.UDPPacketCodecBinary) + require.NoError(t, err) + require.NoError(t, binaryWriter.WriteMsg(in)) + binaryReader, err := NewUDPPacketReadWriter(&binaryStream, wire.ProtocolV2, wire.UDPPacketCodecBinary) + require.NoError(t, err) + out, err := binaryReader.ReadMsg() + require.NoError(t, err) + require.Equal(t, in.Content, out.(*UDPPacket).Content) + + for _, read := range []func(ReadWriter) error{ + func(rw ReadWriter) error { + _, err := rw.ReadMsg() + return err + }, + func(rw ReadWriter) error { + return rw.ReadMsgInto(&UDPPacket{}) + }, + } { + var jsonStream bytes.Buffer + require.NoError(t, NewReadWriter(&jsonStream, wire.ProtocolV2).WriteMsg(in)) + negotiatedReader, err := NewUDPPacketReadWriter(&jsonStream, wire.ProtocolV2, wire.UDPPacketCodecBinary) + require.NoError(t, err) + require.ErrorContains(t, read(negotiatedReader), "JSON UDP packet after binary codec negotiation") + } + + var fallbackStream bytes.Buffer + fallbackWriter, err := NewUDPPacketReadWriter(&fallbackStream, wire.ProtocolV2, "") + require.NoError(t, err) + require.NoError(t, fallbackWriter.WriteMsg(in)) + frame, err := wire.NewConn(&fallbackStream).ReadFrame() + require.NoError(t, err) + require.Equal(t, V2TypeUDPPacket, binary.BigEndian.Uint16(frame.Payload[:2])) +} + +func TestNewUDPPacketReadWriterDefaultProtocolUsesV1(t *testing.T) { + var stream bytes.Buffer + rw, err := NewUDPPacketReadWriter(&stream, "", "") + require.NoError(t, err) + require.IsType(t, &V1ReadWriter{}, rw) + require.NoError(t, rw.WriteMsg(&UDPPacket{Content: []byte("legacy")})) + require.Equal(t, TypeUDPPacket, stream.Bytes()[0]) +} + +func TestNewUDPPacketReadWriterRejectsInvalidSelection(t *testing.T) { + for _, tc := range []struct { + name string + wireProtocol string + udpPacketCodec string + errorSubstring string + }{ + { + name: "binary codec over v1", + wireProtocol: wire.ProtocolV1, + udpPacketCodec: wire.UDPPacketCodecBinary, + errorSubstring: "requires wire protocol v2", + }, + { + name: "binary codec over default protocol", + udpPacketCodec: wire.UDPPacketCodecBinary, + errorSubstring: "requires wire protocol v2", + }, + { + name: "unknown v2 codec", + wireProtocol: wire.ProtocolV2, + udpPacketCodec: "unknown", + errorSubstring: "unsupported UDP packet codec", + }, + { + name: "unknown wire protocol", + wireProtocol: "unknown", + errorSubstring: "unsupported wire protocol", + }, + } { + t.Run(tc.name, func(t *testing.T) { + rw, err := NewUDPPacketReadWriter(&bytes.Buffer{}, tc.wireProtocol, tc.udpPacketCodec) + require.Nil(t, rw) + require.ErrorContains(t, err, tc.errorSubstring) + }) + } +} + +func FuzzDecodeUDPPacketBinary(f *testing.F) { + f.Add([]byte{0, 0, 0}) + f.Add([]byte{2, 4, 203, 0, 113, 9, 0xd4, 0x31, 0, 0, 1}) + f.Fuzz(func(t *testing.T, body []byte) { + _, _ = DecodeUDPPacketBinary(body) + }) +} diff --git a/pkg/msg/wire_v2.go b/pkg/msg/wire_v2.go index 8d2cd88d..f3da2330 100644 --- a/pkg/msg/wire_v2.go +++ b/pkg/msg/wire_v2.go @@ -43,6 +43,7 @@ const ( V2TypeNatHoleResp uint16 = 16 V2TypeNatHoleSid uint16 = 17 V2TypeNatHoleReport uint16 = 18 + V2TypeUDPPacketBinary uint16 = 19 ) var v2MsgTypeMap = map[uint16]any{ diff --git a/pkg/msg/wire_v2_test.go b/pkg/msg/wire_v2_test.go index f6f25e55..ea6e8d5d 100644 --- a/pkg/msg/wire_v2_test.go +++ b/pkg/msg/wire_v2_test.go @@ -84,6 +84,9 @@ func TestV2MessageTypeIDsAreStable(t *testing.T) { require.Equal(t, uint16(16), V2TypeNatHoleResp) require.Equal(t, uint16(17), V2TypeNatHoleSid) require.Equal(t, uint16(18), V2TypeNatHoleReport) + require.Equal(t, uint16(19), V2TypeUDPPacketBinary) + _, registered := v2MsgTypeMap[V2TypeUDPPacketBinary] + require.False(t, registered, "binary UDP has a dedicated codec and must not alter generic type registry") } func TestV2MessageFrameEncoding(t *testing.T) { diff --git a/pkg/proto/wire/crypto.go b/pkg/proto/wire/crypto.go index 69aedc69..38916a28 100644 --- a/pkg/proto/wire/crypto.go +++ b/pkg/proto/wire/crypto.go @@ -68,7 +68,8 @@ func NewServerHello(clientHello ClientHello) (ServerHello, error) { return ServerHello{ Selected: ServerSelection{ Message: MessageSelection{ - Codec: MessageCodecJSON, + Codec: MessageCodecJSON, + UDPPacketCodec: selectUDPPacketCodec(clientHello.Capabilities.Message.UDPPacketCodecs), }, Crypto: CryptoSelection{ Algorithm: algorithm, @@ -92,6 +93,15 @@ func ValidateServerHelloForClient(clientHello ClientHello, serverHello ServerHel if serverHello.Selected.Message.Codec != MessageCodecJSON { return fmt.Errorf("unsupported selected message codec: %s", serverHello.Selected.Message.Codec) } + udpPacketCodec := serverHello.Selected.Message.UDPPacketCodec + if udpPacketCodec != "" { + if udpPacketCodec != UDPPacketCodecBinary { + return fmt.Errorf("unsupported selected UDP packet codec: %s", udpPacketCodec) + } + if !Supports(clientHello.Capabilities.Message.UDPPacketCodecs, udpPacketCodec) { + return fmt.Errorf("selected UDP packet codec was not advertised by client: %s", udpPacketCodec) + } + } cryptoSelection := serverHello.Selected.Crypto if !IsSupportedAEADAlgorithm(cryptoSelection.Algorithm) { return fmt.Errorf("unknown selected crypto algorithm: %s", cryptoSelection.Algorithm) @@ -105,6 +115,13 @@ func ValidateServerHelloForClient(clientHello ClientHello, serverHello ServerHel return nil } +func selectUDPPacketCodec(codecs []string) string { + if Supports(codecs, UDPPacketCodecBinary) { + return UDPPacketCodecBinary + } + return "" +} + func NewCryptoContext(algorithm string, clientHelloPayload, serverHelloPayload []byte) *CryptoContext { return &CryptoContext{ Algorithm: algorithm, diff --git a/pkg/proto/wire/wire.go b/pkg/proto/wire/wire.go index 47bf5984..5a109d67 100644 --- a/pkg/proto/wire/wire.go +++ b/pkg/proto/wire/wire.go @@ -36,6 +36,7 @@ const ( FrameTypeMessage uint16 = 16 MessageCodecJSON = "json" + UDPPacketCodecBinary = "binary-v1" DefaultMaxFramePayloadSize = 64 * 1024 MagicV2 = "FRP\x00\x02\r\n" @@ -182,7 +183,8 @@ type ClientCapabilities struct { } type MessageCapabilities struct { - Codecs []string `json:"codecs,omitempty"` + Codecs []string `json:"codecs,omitempty"` + UDPPacketCodecs []string `json:"udpPacketCodecs,omitempty"` } type CryptoCapabilities struct { @@ -201,7 +203,8 @@ type ServerSelection struct { } type MessageSelection struct { - Codec string `json:"codec,omitempty"` + Codec string `json:"codec,omitempty"` + UDPPacketCodec string `json:"udpPacketCodec,omitempty"` } type CryptoSelection struct { @@ -214,7 +217,8 @@ func clientHelloWithCryptoRandom(bootstrap BootstrapInfo, clientRandom []byte) C Bootstrap: bootstrap, Capabilities: ClientCapabilities{ Message: MessageCapabilities{ - Codecs: []string{MessageCodecJSON}, + Codecs: []string{MessageCodecJSON}, + UDPPacketCodecs: []string{UDPPacketCodecBinary}, }, Crypto: CryptoCapabilities{ Algorithms: PreferredAEADAlgorithms(), diff --git a/pkg/proto/wire/wire_test.go b/pkg/proto/wire/wire_test.go index b564f712..b1c1b062 100644 --- a/pkg/proto/wire/wire_test.go +++ b/pkg/proto/wire/wire_test.go @@ -148,10 +148,40 @@ func TestNewServerHelloSelectsFirstSupportedAEADAlgorithm(t *testing.T) { serverHello, err := NewServerHello(hello) require.NoError(t, err) require.Equal(t, MessageCodecJSON, serverHello.Selected.Message.Codec) + require.Equal(t, UDPPacketCodecBinary, serverHello.Selected.Message.UDPPacketCodec) require.Equal(t, AEADAlgorithmXChaCha20Poly1305, serverHello.Selected.Crypto.Algorithm) require.Len(t, serverHello.Selected.Crypto.ServerRandom, CryptoRandomSize) } +func TestUDPPacketCodecNegotiationFallbackAndValidation(t *testing.T) { + hello := mustClientHello(t, BootstrapInfo{}) + serverHello, err := NewServerHello(hello) + require.NoError(t, err) + require.Equal(t, UDPPacketCodecBinary, serverHello.Selected.Message.UDPPacketCodec) + require.NoError(t, ValidateServerHelloForClient(hello, serverHello)) + + legacyHello := hello + legacyHello.Capabilities.Message.UDPPacketCodecs = nil + legacyServerHello, err := NewServerHello(legacyHello) + require.NoError(t, err) + require.Empty(t, legacyServerHello.Selected.Message.UDPPacketCodec) + require.NoError(t, ValidateServerHelloForClient(legacyHello, legacyServerHello)) + + unknownOffer := hello + unknownOffer.Capabilities.Message.UDPPacketCodecs = []string{"unknown"} + unknownServerHello, err := NewServerHello(unknownOffer) + require.NoError(t, err) + require.Empty(t, unknownServerHello.Selected.Message.UDPPacketCodec) + + rejected := serverHello + rejected.Selected.Message.UDPPacketCodec = "unknown" + require.ErrorContains(t, ValidateServerHelloForClient(hello, rejected), "unsupported selected UDP packet codec") + + unadvertised := serverHello + unadvertised.Selected.Message.UDPPacketCodec = UDPPacketCodecBinary + require.ErrorContains(t, ValidateServerHelloForClient(legacyHello, unadvertised), "was not advertised") +} + func TestNewClientCryptoContextValidatesServerHello(t *testing.T) { hello := mustClientHello(t, BootstrapInfo{}) serverHello, err := NewServerHello(hello) diff --git a/server/control.go b/server/control.go index 6a5d3ce1..ea680d63 100644 --- a/server/control.go +++ b/server/control.go @@ -368,7 +368,8 @@ type SessionContext struct { // server configuration ServerCfg *v1.ServerConfig // negotiated wire protocol for this client session - WireProtocol string + WireProtocol string + UDPPacketCodec string } type controlState uint8 @@ -821,6 +822,7 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err ServerCfg: ctl.sessionCtx.ServerCfg, EncryptionKey: ctl.sessionCtx.EncryptionKey, WireProtocol: ctl.sessionCtx.WireProtocol, + UDPPacketCodec: ctl.sessionCtx.UDPPacketCodec, }) if err != nil { return remoteAddr, err diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index e9ace046..046794b6 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -82,19 +82,20 @@ type Proxy interface { } type BaseProxy struct { - name string - rc *controller.ResourceController - listeners []net.Listener - usedPortsNum int - poolCount int - getWorkConnFn GetWorkConnFn - serverCfg *v1.ServerConfig - encryptionKey []byte - limiter *rate.Limiter - userInfo plugin.UserInfo - loginMsg *msg.Login - configurer v1.ProxyConfigurer - wireProtocol string + name string + rc *controller.ResourceController + listeners []net.Listener + usedPortsNum int + poolCount int + getWorkConnFn GetWorkConnFn + serverCfg *v1.ServerConfig + encryptionKey []byte + limiter *rate.Limiter + userInfo plugin.UserInfo + loginMsg *msg.Login + configurer v1.ProxyConfigurer + wireProtocol string + udpPacketCodec string mu sync.RWMutex xl *xlog.Logger @@ -469,6 +470,7 @@ type Options struct { ServerCfg *v1.ServerConfig EncryptionKey []byte WireProtocol string + UDPPacketCodec string } func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) { @@ -482,20 +484,21 @@ func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) { } basePxy := BaseProxy{ - name: configurer.GetBaseConfig().Name, - rc: options.ResourceController, - listeners: make([]net.Listener, 0), - poolCount: options.PoolCount, - getWorkConnFn: options.GetWorkConnFn, - serverCfg: options.ServerCfg, - encryptionKey: options.EncryptionKey, - limiter: limiter, - xl: xl, - ctx: xlog.NewContext(ctx, xl), - userInfo: options.UserInfo, - loginMsg: options.LoginMsg, - configurer: configurer, - wireProtocol: options.WireProtocol, + name: configurer.GetBaseConfig().Name, + rc: options.ResourceController, + listeners: make([]net.Listener, 0), + poolCount: options.PoolCount, + getWorkConnFn: options.GetWorkConnFn, + serverCfg: options.ServerCfg, + encryptionKey: options.EncryptionKey, + limiter: limiter, + xl: xl, + ctx: xlog.NewContext(ctx, xl), + userInfo: options.UserInfo, + loginMsg: options.LoginMsg, + configurer: configurer, + wireProtocol: options.WireProtocol, + udpPacketCodec: options.UDPPacketCodec, } factory := proxyFactoryRegistry[reflect.TypeOf(configurer)] diff --git a/server/proxy/udp.go b/server/proxy/udp.go index 609d9c42..47bdfc8c 100644 --- a/server/proxy/udp.go +++ b/server/proxy/udp.go @@ -224,7 +224,13 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { pxy.workConn = netpkg.WrapReadWriteCloserToConn(rwc, workConn) // Plain UDP payload follows the negotiated wire protocol for message framing. - payloadConn := msg.NewConn(pxy.workConn, msg.NewReadWriter(pxy.workConn, pxy.wireProtocol)) + payloadRW, err := msg.NewUDPPacketReadWriter(pxy.workConn, pxy.wireProtocol, pxy.udpPacketCodec) + if err != nil { + xl.Errorf("create UDP packet read writer: %v", err) + pxy.workConn.Close() + continue + } + payloadConn := msg.NewConn(pxy.workConn, payloadRW) ctx, cancel := context.WithCancel(context.Background()) go workConnReaderFn(payloadConn) go workConnSenderFn(payloadConn, ctx) diff --git a/server/service.go b/server/service.go index 0a174052..e24acf66 100644 --- a/server/service.go +++ b/server/service.go @@ -470,7 +470,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, interna } } if err == nil { - ctl, err = svr.RegisterControl(controlConn, m, internal, acceptedConn.wireProtocol) + ctl, err = svr.RegisterControl(controlConn, m, internal, acceptedConn.wireProtocol, acceptedConn.udpPacketCodec) } } @@ -509,7 +509,12 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, interna return } case *msg.NewWorkConn: - if err := svr.RegisterWorkConn(acceptedConn.conn, m); err != nil { + if err := svr.RegisterWorkConn( + acceptedConn.conn, + m, + acceptedConn.wireProtocol, + acceptedConn.clientHelloPresent, + ); err != nil { _ = acceptedConn.conn.WriteMsg(&msg.StartWorkConn{ Error: util.GenerateResponseErrorString("invalid NewWorkConn", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)), }) @@ -547,10 +552,12 @@ func (svr *Service) completeControlLogin(ctl *Control, writeSuccess func() error } type acceptedConnection struct { - conn *msg.Conn - wireProtocol string - cryptoContext *wire.CryptoContext - firstMsg msg.Message + conn *msg.Conn + wireProtocol string + clientHelloPresent bool + udpPacketCodec string + cryptoContext *wire.CryptoContext + firstMsg msg.Message } func (svr *Service) acceptConnection(ctx context.Context, conn net.Conn) (*acceptedConnection, error) { @@ -618,6 +625,7 @@ func (ac *acceptedConnection) readFirstV2Msg(conn net.Conn, wireConn *wire.Conn) return nil, fmt.Errorf("read v2 frame: %w", err) } if frame.Type == wire.FrameTypeClientHello { + ac.clientHelloPresent = true if err := ac.handleClientHello(conn, wireConn, frame); err != nil { return nil, err } @@ -666,6 +674,7 @@ func (ac *acceptedConnection) handleClientHello(conn net.Conn, wireConn *wire.Co return fmt.Errorf("write ServerHello: %w", err) } ac.cryptoContext = cryptoContext + ac.udpPacketCodec = serverHello.Selected.Message.UDPPacketCodec return nil } @@ -759,7 +768,20 @@ func (svr *Service) RegisterControl( loginMsg *msg.Login, internal bool, wireProtocol string, + udpPacketCodec string, ) (*Control, error) { + switch wireProtocol { + case wire.ProtocolV1: + if udpPacketCodec != "" { + return nil, fmt.Errorf("UDP packet codec %q requires wire protocol v2", udpPacketCodec) + } + case wire.ProtocolV2: + if udpPacketCodec != "" && udpPacketCodec != wire.UDPPacketCodecBinary { + return nil, fmt.Errorf("unsupported UDP packet codec selection: %s", udpPacketCodec) + } + default: + return nil, fmt.Errorf("unsupported wire protocol: %s", wireProtocol) + } // If client's RunID is empty, it's a new client, we just create a new controller. // Otherwise, we check if there is one controller has the same run id. If so, we release previous controller and start new one. var err error @@ -787,15 +809,16 @@ func (svr *Service) RegisterControl( } ctl, err := NewControl(ctx, &SessionContext{ - RC: svr.rc, - PxyManager: svr.pxyManager, - PluginManager: svr.pluginManager, - AuthVerifier: authVerifier, - EncryptionKey: svr.auth.EncryptionKey(), - Conn: ctlConn, - LoginMsg: loginMsg, - ServerCfg: svr.cfg, - WireProtocol: wireProtocol, + RC: svr.rc, + PxyManager: svr.pxyManager, + PluginManager: svr.pluginManager, + AuthVerifier: authVerifier, + EncryptionKey: svr.auth.EncryptionKey(), + Conn: ctlConn, + LoginMsg: loginMsg, + ServerCfg: svr.cfg, + WireProtocol: wireProtocol, + UDPPacketCodec: udpPacketCodec, }) if err != nil { xl.Warnf("create new controller error: %v", err) @@ -820,13 +843,24 @@ func (svr *Service) RegisterControl( } // RegisterWorkConn register a new work connection to control and proxies need it. -func (svr *Service) RegisterWorkConn(workConn *msg.Conn, newMsg *msg.NewWorkConn) error { +func (svr *Service) RegisterWorkConn( + workConn *msg.Conn, + newMsg *msg.NewWorkConn, + workWireProtocol string, + workClientHelloPresent bool, +) error { + if workClientHelloPresent { + return fmt.Errorf("ClientHello is not allowed on work connections") + } xl := netpkg.NewLogFromConn(workConn) ctl, exist := svr.ctlManager.GetByID(newMsg.RunID) if !exist { xl.Warnf("no client control found for run id [%s]", newMsg.RunID) return fmt.Errorf("no client control found for run id [%s]", newMsg.RunID) } + if workWireProtocol != ctl.sessionCtx.WireProtocol { + return fmt.Errorf("work connection wire protocol mismatch: got %s want %s", workWireProtocol, ctl.sessionCtx.WireProtocol) + } // server plugin hook content := &plugin.NewWorkConnContent{ diff --git a/server/service_test.go b/server/service_test.go index 0ebec604..20769725 100644 --- a/server/service_test.go +++ b/server/service_test.go @@ -79,6 +79,70 @@ func TestWriteWithDeadlineTimesOutAndClearsDeadline(t *testing.T) { } } +func TestServiceAcceptConnectionTracksClientHelloPresence(t *testing.T) { + for _, tc := range []struct { + name string + clientHelloPresent bool + offeredCodecs []string + expectedCodec string + }{ + { + name: "absent Hello", + }, + { + name: "present Hello with JSON fallback", + clientHelloPresent: true, + }, + { + name: "present Hello with binary codec", + clientHelloPresent: true, + offeredCodecs: []string{wire.UDPPacketCodecBinary}, + expectedCodec: wire.UDPPacketCodecBinary, + }, + } { + t.Run(tc.name, func(t *testing.T) { + serverConn, clientConn := net.Pipe() + defer serverConn.Close() + defer clientConn.Close() + + clientErrCh := make(chan error, 1) + go func() { + if err := wire.WriteMagic(clientConn); err != nil { + clientErrCh <- err + return + } + wireConn := wire.NewConn(clientConn) + if tc.clientHelloPresent { + hello, err := wire.NewClientHello(wire.BootstrapInfo{}) + if err != nil { + clientErrCh <- err + return + } + hello.Capabilities.Message.UDPPacketCodecs = tc.offeredCodecs + if err := wireConn.WriteJSONFrame(wire.FrameTypeClientHello, hello); err != nil { + clientErrCh <- err + return + } + var serverHello wire.ServerHello + if err := wireConn.ReadJSONFrame(wire.FrameTypeServerHello, &serverHello); err != nil { + clientErrCh <- err + return + } + } + clientErrCh <- msg.NewV2ReadWriterWithConn(wireConn).WriteMsg(&msg.NewWorkConn{RunID: "shared-run"}) + }() + + acceptedConn, err := (&Service{}).acceptConnection(t.Context(), serverConn) + require.NoError(t, err) + require.NoError(t, <-clientErrCh) + require.Equal(t, tc.clientHelloPresent, acceptedConn.clientHelloPresent) + require.Equal(t, tc.expectedCodec, acceptedConn.udpPacketCodec) + require.IsType(t, &msg.NewWorkConn{}, acceptedConn.firstMsg) + require.NoError(t, acceptedConn.conn.Close()) + }) + } +} + func TestSharedPortHTTPListenerProtocols(t *testing.T) { listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) @@ -428,7 +492,7 @@ func TestServiceWorkConnRoutingRequiresCurrentRunningControl(t *testing.T) { pendingConn := newCountingCloseConn() pendingMsgConn := msg.NewConn(pendingConn, msg.NewV1ReadWriter(pendingConn)) - err = registerWorkConnAsCaller(svr, pendingMsgConn, &msg.NewWorkConn{RunID: "shared-run"}) + err = registerWorkConnAsCaller(svr, pendingMsgConn, &msg.NewWorkConn{RunID: "shared-run"}, wire.ProtocolV1, false) require.Error(t, err) require.Equal(t, int64(1), pendingConn.closeCount.Load()) require.Len(t, ctl.workConnCh, 0) @@ -442,7 +506,7 @@ func TestServiceWorkConnRoutingRequiresCurrentRunningControl(t *testing.T) { runningConn := newCountingCloseConn() runningMsgConn := msg.NewConn(runningConn, msg.NewV1ReadWriter(runningConn)) - require.NoError(t, svr.RegisterWorkConn(runningMsgConn, &msg.NewWorkConn{RunID: "shared-run"})) + require.NoError(t, svr.RegisterWorkConn(runningMsgConn, &msg.NewWorkConn{RunID: "shared-run"}, wire.ProtocolV1, false)) require.Len(t, ctl.workConnCh, 1) require.NoError(t, ctl.Close()) @@ -450,6 +514,123 @@ func TestServiceWorkConnRoutingRequiresCurrentRunningControl(t *testing.T) { require.Equal(t, int64(1), runningConn.closeCount.Load()) } +func TestServiceWorkConnRoutingRejectsWireProtocolMismatch(t *testing.T) { + svr := newControlTestService(t) + ctl, controlConn, err := registerLifecycleTestControl(svr) + require.NoError(t, err) + require.NoError(t, svr.completeControlLogin(ctl, func() error { return nil })) + waitForSignal(t, controlConn.readStarted, "control reader to start") + + workConn := newCountingCloseConn() + workMsgConn := msg.NewConn(workConn, msg.NewV2ReadWriter(workConn)) + err = svr.RegisterWorkConn(workMsgConn, &msg.NewWorkConn{RunID: "shared-run"}, wire.ProtocolV2, false) + require.ErrorContains(t, err, "wire protocol mismatch") + require.Len(t, ctl.workConnCh, 0) + _ = workMsgConn.Close() + require.NoError(t, ctl.Close()) +} + +func TestServiceWorkConnRoutingClientHelloPolicy(t *testing.T) { + for _, tc := range []struct { + name string + controlUDPPacketCodec string + workClientHelloPresent bool + errorSubstring string + }{ + { + name: "JSON control allows work connection without Hello", + }, + { + name: "binary control allows work connection without Hello", + controlUDPPacketCodec: wire.UDPPacketCodecBinary, + }, + { + name: "JSON control rejects work connection with Hello", + workClientHelloPresent: true, + errorSubstring: "ClientHello is not allowed", + }, + { + name: "binary control rejects work connection with Hello", + controlUDPPacketCodec: wire.UDPPacketCodecBinary, + workClientHelloPresent: true, + errorSubstring: "ClientHello is not allowed", + }, + } { + t.Run(tc.name, func(t *testing.T) { + svr := newControlTestService(t) + controlConn := newDeadlineReadConn() + controlMsgConn := msg.NewConn(controlConn, msg.NewV2ReadWriter(controlConn)) + ctl, err := svr.RegisterControl(controlMsgConn, &msg.Login{ + RunID: "shared-run", + ClientID: "client", + ClientSpec: msg.ClientSpec{ + AlwaysAuthPass: true, + }, + }, true, wire.ProtocolV2, tc.controlUDPPacketCodec) + require.NoError(t, err) + require.NoError(t, svr.completeControlLogin(ctl, func() error { return nil })) + waitForSignal(t, controlConn.readStarted, "control reader to start") + + workConn := newCountingCloseConn() + workMsgConn := msg.NewConn(workConn, msg.NewV2ReadWriter(workConn)) + err = svr.RegisterWorkConn( + workMsgConn, + &msg.NewWorkConn{RunID: "shared-run"}, + wire.ProtocolV2, + tc.workClientHelloPresent, + ) + if tc.errorSubstring != "" { + require.ErrorContains(t, err, tc.errorSubstring) + require.Len(t, ctl.workConnCh, 0) + require.NoError(t, workMsgConn.Close()) + } else { + require.NoError(t, err) + require.Len(t, ctl.workConnCh, 1) + } + + require.NoError(t, ctl.Close()) + waitForControlDone(t, ctl) + require.Equal(t, int64(1), workConn.closeCount.Load()) + }) + } +} + +func TestServiceRegisterControlRejectsInvalidCodecSelection(t *testing.T) { + for _, tc := range []struct { + name string + wireProtocol string + udpPacketCodec string + errorSubstring string + }{ + { + name: "binary codec over v1", + wireProtocol: wire.ProtocolV1, + udpPacketCodec: wire.UDPPacketCodecBinary, + errorSubstring: "requires wire protocol v2", + }, + { + name: "unknown v2 codec", + wireProtocol: wire.ProtocolV2, + udpPacketCodec: "unknown", + errorSubstring: "unsupported UDP packet codec", + }, + { + name: "unknown wire protocol", + wireProtocol: "unknown", + errorSubstring: "unsupported wire protocol", + }, + } { + t.Run(tc.name, func(t *testing.T) { + svr := newControlTestService(t) + conn := newDeadlineReadConn() + msgConn := msg.NewConn(conn, msg.NewV1ReadWriter(conn)) + ctl, err := svr.RegisterControl(msgConn, &msg.Login{}, true, tc.wireProtocol, tc.udpPacketCodec) + require.Nil(t, ctl) + require.ErrorContains(t, err, tc.errorSubstring) + }) + } +} + func TestServiceWorkConnRoutingRejectsLostGeneration(t *testing.T) { for _, action := range []string{"replace", "close"} { t.Run(action, func(t *testing.T) { @@ -465,7 +646,7 @@ func TestServiceWorkConnRoutingRejectsLostGeneration(t *testing.T) { workMsgConn := msg.NewConn(workConn, msg.NewV1ReadWriter(workConn)) routeDone := make(chan error, 1) go func() { - routeDone <- registerWorkConnAsCaller(svr, workMsgConn, &msg.NewWorkConn{RunID: "shared-run"}) + routeDone <- registerWorkConnAsCaller(svr, workMsgConn, &msg.NewWorkConn{RunID: "shared-run"}, wire.ProtocolV1, false) }() waitForSignal(t, barrier.entered, "work connection plugin barrier") @@ -509,7 +690,7 @@ func TestServiceVisitorRoutingExcludesPendingUser(t *testing.T) { ClientSpec: msg.ClientSpec{ AlwaysAuthPass: true, }, - }, true, wire.ProtocolV1) + }, true, wire.ProtocolV1, "") require.NoError(t, err) timestamp := time.Now().Unix() @@ -567,7 +748,7 @@ func registerLifecycleTestControl(svr *Service) (*Control, *deadlineReadConn, er ClientSpec: msg.ClientSpec{ AlwaysAuthPass: true, }, - }, true, wire.ProtocolV1) + }, true, wire.ProtocolV1, "") return ctl, conn, err } @@ -584,8 +765,14 @@ func waitForDifferentCurrentControl(t *testing.T, manager *ControlManager, runID return nil } -func registerWorkConnAsCaller(svr *Service, workConn *msg.Conn, newMsg *msg.NewWorkConn) error { - err := svr.RegisterWorkConn(workConn, newMsg) +func registerWorkConnAsCaller( + svr *Service, + workConn *msg.Conn, + newMsg *msg.NewWorkConn, + wireProtocol string, + clientHelloPresent bool, +) error { + err := svr.RegisterWorkConn(workConn, newMsg, wireProtocol, clientHelloPresent) if err != nil { _ = workConn.Close() } diff --git a/test/e2e/compatibility/compatibility_test.go b/test/e2e/compatibility/compatibility_test.go index 246bcac1..ad505510 100644 --- a/test/e2e/compatibility/compatibility_test.go +++ b/test/e2e/compatibility/compatibility_test.go @@ -192,6 +192,41 @@ transport.wireProtocol = "v2" }) }) +var _ = ginkgo.Describe("[Compatibility: BinaryUDPPacket]", func() { + f := framework.NewDefaultFramework() + + ginkgo.BeforeEach(func() { + supportsV2, knownVersion := baselineSupportsControlWireProtocolV2(compatCtx.BaselineVersion) + if !knownVersion || !supportsV2 { + ginkgo.Skip(fmt.Sprintf("baseline version %q does not have known wire protocol v2 support", compatCtx.BaselineVersion)) + } + }) + + ginkgo.It("current frps falls back to JSON for baseline frpc", func() { + portName := port.GenName("CompatBinaryUDPBaselineFRPC") + clientConf := udpClientConfig("udp", portName, `transport.wireProtocol = "v2"`) + f.RunProcessesWithBinaries( + compatCtx.CurrentFRPSPath, + compatCtx.BaselineFRPCPath, + consts.DefaultServerConfig, + []string{clientConf}, + ) + framework.NewRequestExpect(f).Protocol("udp").PortName(portName).Ensure() + }) + + ginkgo.It("current frpc falls back to JSON for baseline frps", func() { + portName := port.GenName("CompatBinaryUDPBaselineFRPS") + clientConf := udpClientConfig("udp", portName, `transport.wireProtocol = "v2"`) + f.RunProcessesWithBinaries( + compatCtx.BaselineFRPSPath, + compatCtx.CurrentFRPCPath, + consts.DefaultServerConfig, + []string{clientConf}, + ) + framework.NewRequestExpect(f).Protocol("udp").PortName(portName).Ensure() + }) +}) + func tcpClientConfig(proxyName string, remotePortName string, extra string) string { return fmt.Sprintf(` serverAddr = "127.0.0.1" @@ -208,6 +243,22 @@ remotePort = {{ .%s }} `, consts.PortServerName, extra, proxyName, framework.TCPEchoServerPort, remotePortName) } +func udpClientConfig(proxyName string, remotePortName string, extra string) string { + return fmt.Sprintf(` +serverAddr = "127.0.0.1" +serverPort = {{ .%s }} +loginFailExit = true +log.level = "trace" +%s + +[[proxies]] +name = "%s" +type = "udp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, consts.PortServerName, extra, proxyName, framework.UDPEchoServerPort, remotePortName) +} + func expectProcessExit(p *process.Process, timeout time.Duration) { select { case <-p.Done(): diff --git a/test/e2e/v1/basic/wire.go b/test/e2e/v1/basic/wire.go index 051e6a6c..cb61558f 100644 --- a/test/e2e/v1/basic/wire.go +++ b/test/e2e/v1/basic/wire.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "time" "github.com/onsi/ginkgo/v2" @@ -204,6 +205,87 @@ var _ = ginkgo.Describe("[Feature: WireProtocol]", func() { }) }) +var _ = ginkgo.Describe("[Feature: BinaryUDPPacket]", func() { + f := framework.NewDefaultFramework() + + for _, tc := range []struct { + name string + protocol string + extraServer string + extraTransport string + }{ + {name: "tcp mux on", protocol: "tcp", extraTransport: "transport.tcpMux = true"}, + {name: "tcp mux off", protocol: "tcp", extraServer: "transport.tcpMux = false", extraTransport: "transport.tcpMux = false"}, + {name: "kcp", protocol: "kcp"}, + {name: "quic stream", protocol: "quic"}, + {name: "websocket", protocol: "websocket"}, + } { + ginkgo.It(tc.name, func() { + runClientServerTest(f, &generalTestConfigures{ + server: renderBindPortConfig(tc.protocol) + "\n" + tc.extraServer, + client: fmt.Sprintf(` + transport.wireProtocol = "v2" + transport.protocol = %q + %s + `, tc.protocol, tc.extraTransport), + }) + }) + } + + ginkgo.It("wss", func() { + wssPort := f.AllocPort() + runClientServerTest(f, &generalTestConfigures{ + clientPrefix: fmt.Sprintf(` + serverAddr = "127.0.0.1" + serverPort = %d + loginFailExit = false + transport.protocol = "wss" + transport.wireProtocol = "v2" + log.level = "trace" + `, wssPort), + client2: fmt.Sprintf(` + [[proxies]] + name = "wss2ws" + type = "tcp" + remotePort = %d + [proxies.plugin] + type = "https2http" + localAddr = "127.0.0.1:{{ .%s }}" + `, wssPort, consts.PortServerName), + testDelay: 10 * time.Second, + }) + }) + + for _, tc := range []struct { + name string + transport string + }{ + {name: "plain"}, + {name: "aes-cfb", transport: "transport.useEncryption = true"}, + {name: "snappy", transport: "transport.useCompression = true"}, + {name: "snappy and aes-cfb", transport: "transport.useEncryption = true\ntransport.useCompression = true"}, + {name: "limiter", transport: "transport.bandwidthLimit = \"1MB\""}, + } { + ginkgo.It(tc.name, func() { + serverConf := consts.DefaultServerConfig + udpPortName := port.GenName("BinaryUDPPacket") + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + transport.wireProtocol = "v2" + + [[proxies]] + name = "udp" + type = "udp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + %s + `, framework.UDPEchoServerPort, udpPortName, tc.transport) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.NewRequestExpect(f).Protocol("udp").PortName(udpPortName).Ensure() + }) + } +}) + type wireClientInfo struct { ClientID string `json:"clientID"` WireProtocol string `json:"wireProtocol"` From 9d45a557200fcbc6a4598b5ee0d75c405299c521 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 30 Jul 2026 14:40:54 +0800 Subject: [PATCH 10/23] docs: add release note for #5456 (#5458) --- Release.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Release.md b/Release.md index 6f209a78..803ef5e4 100644 --- a/Release.md +++ b/Release.md @@ -1,5 +1,3 @@ -## Fixes +## Features -* HTTP vhost servers no longer support HTTP/1.1 `Upgrade: h2c` requests. Cleartext HTTP/2 prior-knowledge remains supported. -* Fixed control-session replacement leaks when frpc reconnects through a half-open TCP multiplexed connection. -* Fixed an SSH tunnel gateway panic when handling malformed exec requests. +* Ordinary UDP proxy payloads now use a dedicated binary codec when frpc and frps successfully negotiate the capability under wire protocol v2, reducing frame size and encoding/decoding overhead. Wire protocol v1 and SUDP remain unchanged; wire protocol v2 falls back to JSON `UDPPacket` when the peer does not support or did not negotiate the capability. From 1ab59e763ca1be1f6c5174cf596f556fbcb9417d Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 30 Jul 2026 23:30:16 +0800 Subject: [PATCH 11/23] server: validate control pool counts (#5459) --- Release.md | 4 +- pkg/config/v1/server.go | 2 +- pkg/config/v1/validation/server.go | 3 ++ pkg/config/v1/validation/server_test.go | 51 ++++++++++++++++++++++++ server/control.go | 24 +++++++++++- server/control_test.go | 52 +++++++++++++++++++++++++ server/service_test.go | 43 ++++++++++++++++++++ 7 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 pkg/config/v1/validation/server_test.go diff --git a/Release.md b/Release.md index 803ef5e4..26deaad0 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,3 @@ -## Features +## Fixes -* Ordinary UDP proxy payloads now use a dedicated binary codec when frpc and frps successfully negotiate the capability under wire protocol v2, reducing frame size and encoding/decoding overhead. Wire protocol v1 and SUDP remain unchanged; wire protocol v2 falls back to JSON `UDPPacket` when the peer does not support or did not negotiate the capability. +* Fixed a server panic and remote denial of service caused by a client sending a negative `pool_count`. Negative values are now rejected before work-connection pool resources are allocated. diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go index a92aac97..67c1d84b 100644 --- a/pkg/config/v1/server.go +++ b/pkg/config/v1/server.go @@ -167,7 +167,7 @@ type ServerTransportConfig struct { // If negative, keep-alive probes are disabled. TCPKeepAlive int64 `json:"tcpKeepalive,omitempty"` // MaxPoolCount specifies the maximum pool size for each proxy. By default, - // this value is 5. + // this value is 5. Negative values are invalid. MaxPoolCount int64 `json:"maxPoolCount,omitempty"` // HeartBeatTimeout specifies the maximum time to wait for a heartbeat // before terminating the connection. It is not recommended to change this diff --git a/pkg/config/v1/validation/server.go b/pkg/config/v1/validation/server.go index 8be740aa..54bd8348 100644 --- a/pkg/config/v1/validation/server.go +++ b/pkg/config/v1/validation/server.go @@ -51,6 +51,9 @@ func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, err errs = AppendError(errs, ValidatePort(c.VhostHTTPPort, "vhostHTTPPort")) errs = AppendError(errs, ValidatePort(c.VhostHTTPSPort, "vhostHTTPSPort")) errs = AppendError(errs, ValidatePort(c.TCPMuxHTTPConnectPort, "tcpMuxHTTPConnectPort")) + if c.Transport.MaxPoolCount < 0 { + errs = AppendError(errs, fmt.Errorf("invalid transport.maxPoolCount, must be non-negative")) + } for _, p := range c.HTTPPlugins { if !lo.Every(SupportedHTTPPluginOps, p.Ops) { diff --git a/pkg/config/v1/validation/server_test.go b/pkg/config/v1/validation/server_test.go new file mode 100644 index 00000000..7c0c57ad --- /dev/null +++ b/pkg/config/v1/validation/server_test.go @@ -0,0 +1,51 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validation + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func TestValidateServerConfigMaxPoolCount(t *testing.T) { + for _, tc := range []struct { + name string + maxPoolCount int64 + wantErr bool + }{ + {name: "negative", maxPoolCount: -1, wantErr: true}, + {name: "zero", maxPoolCount: 0}, + {name: "positive", maxPoolCount: 5}, + {name: "maximum int64", maxPoolCount: math.MaxInt64}, + } { + t.Run(tc.name, func(t *testing.T) { + cfg := validServerConfigWithAuth(v1.AuthServerConfig{Method: v1.AuthMethodToken}) + cfg.Transport.MaxPoolCount = tc.maxPoolCount + require.NoError(t, cfg.Complete()) + + _, err := NewConfigValidator(nil).ValidateServerConfig(cfg) + if tc.wantErr { + require.ErrorContains(t, err, "invalid transport.maxPoolCount") + require.ErrorContains(t, err, "must be non-negative") + return + } + require.NoError(t, err) + }) + } +} diff --git a/server/control.go b/server/control.go index ea680d63..1690b01b 100644 --- a/server/control.go +++ b/server/control.go @@ -17,6 +17,7 @@ package server import ( "context" "fmt" + "math" "net" "runtime/debug" "sync" @@ -45,6 +46,8 @@ type ControlID uint64 var nextControlID atomic.Uint64 +const workConnPoolCapacityOffset = 10 + type controlEntry struct { ctl *Control id ControlID @@ -431,10 +434,27 @@ type Control struct { } func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) { - poolCount := min(sessionCtx.LoginMsg.PoolCount, int(sessionCtx.ServerCfg.Transport.MaxPoolCount)) + if sessionCtx.LoginMsg.PoolCount < 0 { + return nil, fmt.Errorf("invalid pool count %d, must be non-negative", sessionCtx.LoginMsg.PoolCount) + } + if sessionCtx.ServerCfg.Transport.MaxPoolCount < 0 { + return nil, fmt.Errorf( + "invalid max pool count %d, must be non-negative", + sessionCtx.ServerCfg.Transport.MaxPoolCount, + ) + } + effectivePoolCount := min(int64(sessionCtx.LoginMsg.PoolCount), sessionCtx.ServerCfg.Transport.MaxPoolCount) + maxPoolCountForChannel := int64(math.MaxInt) - int64(workConnPoolCapacityOffset) + if effectivePoolCount > maxPoolCountForChannel { + return nil, fmt.Errorf( + "invalid effective pool count %d, cannot safely add %d for work connection pool capacity", + effectivePoolCount, workConnPoolCapacityOffset, + ) + } + poolCount := int(effectivePoolCount) ctl := &Control{ sessionCtx: sessionCtx, - workConnCh: make(chan *proxy.WorkConn, poolCount+10), + workConnCh: make(chan *proxy.WorkConn, poolCount+workConnPoolCapacityOffset), proxies: make(map[string]proxy.Proxy), poolCount: poolCount, portsUsedNum: 0, diff --git a/server/control_test.go b/server/control_test.go index 9dce3d24..965ea5ea 100644 --- a/server/control_test.go +++ b/server/control_test.go @@ -17,6 +17,7 @@ package server import ( "context" "errors" + "math" "net" "os" "sync" @@ -52,6 +53,57 @@ func TestControlPendingReplacementFinishesWithoutStarting(t *testing.T) { require.Equal(t, int64(0), metrics.closedClients()) } +func TestNewControlPoolCountBoundaries(t *testing.T) { + for _, tc := range []struct { + name string + poolCount int + maxPoolCount int64 + wantErr string + wantPoolCount int + wantCapacity int + }{ + {name: "negative pool count below offset", poolCount: -11, maxPoolCount: 5, wantErr: "invalid pool count"}, + {name: "negative pool count at offset", poolCount: -10, maxPoolCount: 5, wantErr: "invalid pool count"}, + {name: "negative pool count", poolCount: -1, maxPoolCount: 5, wantErr: "invalid pool count"}, + {name: "zero pool count", poolCount: 0, maxPoolCount: 5, wantPoolCount: 0, wantCapacity: 10}, + {name: "pool count capped", poolCount: 10, maxPoolCount: 5, wantPoolCount: 5, wantCapacity: 15}, + {name: "maximum int pool count capped", poolCount: math.MaxInt, maxPoolCount: 5, wantPoolCount: 5, wantCapacity: 15}, + {name: "negative maximum", poolCount: 1, maxPoolCount: -1, wantErr: "invalid max pool count"}, + {name: "maximum int64 with small client pool", poolCount: 1, maxPoolCount: math.MaxInt64, wantPoolCount: 1, wantCapacity: 11}, + {name: "maximum int client and server overflow", poolCount: math.MaxInt, maxPoolCount: math.MaxInt64, wantErr: "cannot safely add"}, + } { + t.Run(tc.name, func(t *testing.T) { + conn := newDeadlineReadConn() + msgConn := msg.NewConn(conn, msg.NewV1ReadWriter(conn)) + cfg := &v1.ServerConfig{} + cfg.Transport.MaxPoolCount = tc.maxPoolCount + + ctl, err := NewControl(context.Background(), &SessionContext{ + RC: &controller.ResourceController{}, + PxyManager: proxy.NewManager(), + PluginManager: plugin.NewManager(), + AuthVerifier: auth.AlwaysPassVerifier, + Conn: msgConn, + LoginMsg: &msg.Login{ + RunID: "pool-count-run", + PoolCount: tc.poolCount, + }, + ServerCfg: cfg, + }) + if tc.wantErr != "" { + require.Nil(t, ctl) + require.ErrorContains(t, err, tc.wantErr) + return + } + + require.NoError(t, err) + require.Equal(t, tc.wantPoolCount, ctl.poolCount) + require.Equal(t, tc.wantCapacity, cap(ctl.workConnCh)) + require.NoError(t, ctl.Close()) + }) + } +} + func TestControlRunningReplacementFinishesInWorker(t *testing.T) { clientRegistry := registry.NewClientRegistry() manager := NewControlManager(clientRegistry) diff --git a/server/service_test.go b/server/service_test.go index 20769725..cc7645f8 100644 --- a/server/service_test.go +++ b/server/service_test.go @@ -17,6 +17,7 @@ package server import ( "context" "errors" + "math" "net" "net/http" "runtime" @@ -631,6 +632,48 @@ func TestServiceRegisterControlRejectsInvalidCodecSelection(t *testing.T) { } } +func TestServiceRegisterControlPoolCountBoundaries(t *testing.T) { + for _, tc := range []struct { + name string + poolCount int + wantErr bool + wantPool int + }{ + {name: "less than channel offset", poolCount: -11, wantErr: true}, + {name: "channel offset", poolCount: -10, wantErr: true}, + {name: "negative", poolCount: -1, wantErr: true}, + {name: "zero", poolCount: 0, wantPool: 0}, + {name: "capped by server maximum", poolCount: 10, wantPool: 5}, + {name: "maximum int capped", poolCount: math.MaxInt, wantPool: 5}, + } { + t.Run(tc.name, func(t *testing.T) { + svr := newControlTestService(t) + svr.cfg.Transport.MaxPoolCount = 5 + conn := newDeadlineReadConn() + msgConn := msg.NewConn(conn, msg.NewV1ReadWriter(conn)) + const timestamp = int64(1) + + ctl, err := svr.RegisterControl(msgConn, &msg.Login{ + RunID: "pool-count-run", + ClientID: "client", + Timestamp: timestamp, + PrivilegeKey: util.GetAuthKey("", timestamp), + PoolCount: tc.poolCount, + }, false, wire.ProtocolV1, "") + if tc.wantErr { + require.Nil(t, ctl) + require.ErrorContains(t, err, "unexpected error when creating new controller") + return + } + + require.NoError(t, err) + require.Equal(t, tc.wantPool, ctl.poolCount) + require.NoError(t, ctl.Close()) + waitForControlDone(t, ctl) + }) + } +} + func TestServiceWorkConnRoutingRejectsLostGeneration(t *testing.T) { for _, action := range []string{"replace", "close"} { t.Run(action, func(t *testing.T) { From 2291e8835f941ff73903ac22efe48035e3e0882d Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 31 Jul 2026 16:53:16 +0800 Subject: [PATCH 12/23] feat: use binary codec for SUDP packets (#5461) --- Release.md | 4 + client/control.go | 2 +- client/proxy/sudp.go | 8 +- client/service_shutdown_test.go | 2 +- client/visitor/sudp.go | 8 +- client/visitor/visitor.go | 11 ++ client/visitor/visitor_manager.go | 11 ++ pkg/msg/udp_benchmark_test.go | 199 +++++++++++++++++++++ server/control.go | 4 +- server/proxy/proxy.go | 68 +++++++- server/proxy/sudp_benchmark_test.go | 232 +++++++++++++++++++++++++ server/proxy/sudp_test.go | 259 +++++++++++++++++++++++++--- server/service.go | 16 +- server/service_test.go | 95 +++++++++- server/visitor/visitor.go | 18 +- server/visitor/visitor_test.go | 11 +- test/e2e/v1/basic/wire.go | 6 +- 17 files changed, 900 insertions(+), 54 deletions(-) create mode 100644 pkg/msg/udp_benchmark_test.go create mode 100644 server/proxy/sudp_benchmark_test.go diff --git a/Release.md b/Release.md index 26deaad0..0d2500d6 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,7 @@ +## Features + +* UDP packet payloads for ordinary UDP proxies and SUDP now use a dedicated binary codec when frpc and frps successfully negotiate the capability under wire protocol v2, using a more compact wire representation. Wire protocol v1 remains JSON; wire protocol v2 falls back to JSON `UDPPacket` when the peer does not support or did not negotiate the capability. + ## Fixes * Fixed a server panic and remote denial of service caused by a client sending a negative `pool_count`. Negative values are now rejected before work-connection pool resources are allocated. diff --git a/client/control.go b/client/control.go index 5980ed34..4a354ff6 100644 --- a/client/control.go +++ b/client/control.go @@ -103,7 +103,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro sessionCtx.UDPPacketCodec, ) ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common, - ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController) + ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController, sessionCtx.UDPPacketCodec) return ctl, nil } diff --git a/client/proxy/sudp.go b/client/proxy/sudp.go index 7db6d991..b5613856 100644 --- a/client/proxy/sudp.go +++ b/client/proxy/sudp.go @@ -87,7 +87,13 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { } workConn := netpkg.WrapReadWriteCloserToConn(remote, conn) - payloadConn := msg.NewConn(workConn, msg.NewReadWriter(workConn, pxy.clientCfg.Transport.WireProtocol)) + payloadRW, err := msg.NewUDPPacketReadWriter(workConn, pxy.clientCfg.Transport.WireProtocol, pxy.udpPacketCodec) + if err != nil { + xl.Errorf("create SUDP packet read writer: %v", err) + _ = workConn.Close() + return + } + payloadConn := msg.NewConn(workConn, payloadRW) readCh := make(chan *msg.UDPPacket, 1024) sendCh := make(chan msg.Message, 1024) isClose := false diff --git a/client/service_shutdown_test.go b/client/service_shutdown_test.go index 3b956726..73c7bd3d 100644 --- a/client/service_shutdown_test.go +++ b/client/service_shutdown_test.go @@ -35,7 +35,7 @@ func newGracefulCloseTestService() *Service { doneCh: make(chan struct{}), } ctl.pm = proxy.NewManager(ctx, common, nil, nil, nil, "") - ctl.vm = visitor.NewManager(ctx, "graceful-close-race", common, nil, nil, nil) + ctl.vm = visitor.NewManager(ctx, "graceful-close-race", common, nil, nil, nil, "") return &Service{ctl: ctl, cancel: context.CancelCauseFunc(func(error) {})} } diff --git a/client/visitor/sudp.go b/client/visitor/sudp.go index 91d57da6..f1939bda 100644 --- a/client/visitor/sudp.go +++ b/client/visitor/sudp.go @@ -113,7 +113,13 @@ func (sv *SUDPVisitor) dispatcher() { func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) { xl := xlog.FromContextSafe(sv.ctx) xl.Debugf("starting sudp proxy worker") - payloadConn := msg.NewConn(workConn, msg.NewReadWriter(workConn, sv.clientCfg.Transport.WireProtocol)) + payloadRW, err := msg.NewUDPPacketReadWriter(workConn, sv.clientCfg.Transport.WireProtocol, udpPacketCodecFromHelper(sv.helper)) + if err != nil { + xl.Errorf("create SUDP packet read writer: %v", err) + _ = workConn.Close() + return + } + payloadConn := msg.NewConn(workConn, payloadRW) wg := &sync.WaitGroup{} wg.Add(2) diff --git a/client/visitor/visitor.go b/client/visitor/visitor.go index dff0bb94..eccad013 100644 --- a/client/visitor/visitor.go +++ b/client/visitor/visitor.go @@ -50,6 +50,17 @@ type Helper interface { RunID() string } +type udpPacketCodecProvider interface { + UDPPacketCodec() string +} + +func udpPacketCodecFromHelper(helper Helper) string { + if provider, ok := helper.(udpPacketCodecProvider); ok { + return provider.UDPPacketCodec() + } + return "" +} + // Visitor is used for forward traffics from local port tot remote service. type Visitor interface { Run() error diff --git a/client/visitor/visitor_manager.go b/client/visitor/visitor_manager.go index 1ca194bd..3e614f7a 100644 --- a/client/visitor/visitor_manager.go +++ b/client/visitor/visitor_manager.go @@ -53,7 +53,12 @@ func NewManager( connectServer func() (*msg.Conn, error), msgTransporter transport.MessageTransporter, vnetController *vnet.Controller, + udpPacketCodecs ...string, ) *Manager { + udpPacketCodec := "" + if len(udpPacketCodecs) > 0 { + udpPacketCodec = udpPacketCodecs[0] + } m := &Manager{ clientCfg: clientCfg, cfgs: make(map[string]v1.VisitorConfigurer), @@ -68,6 +73,7 @@ func NewManager( vnetController: vnetController, transferConnFn: m.TransferConn, runID: runID, + udpPacketCodec: udpPacketCodec, } return m } @@ -205,6 +211,7 @@ type visitorHelperImpl struct { vnetController *vnet.Controller transferConnFn func(name string, conn net.Conn) error runID string + udpPacketCodec string } func (v *visitorHelperImpl) ConnectServer() (*msg.Conn, error) { @@ -226,3 +233,7 @@ func (v *visitorHelperImpl) VNetController() *vnet.Controller { func (v *visitorHelperImpl) RunID() string { return v.runID } + +func (v *visitorHelperImpl) UDPPacketCodec() string { + return v.udpPacketCodec +} diff --git a/pkg/msg/udp_benchmark_test.go b/pkg/msg/udp_benchmark_test.go new file mode 100644 index 00000000..575938ec --- /dev/null +++ b/pkg/msg/udp_benchmark_test.go @@ -0,0 +1,199 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package msg + +import ( + "bytes" + "fmt" + "net" + "runtime" + "testing" + + "github.com/fatedier/frp/pkg/proto/wire" +) + +type udpBenchmarkCase struct { + name string + packet *UDPPacket +} + +var ( + udpBenchmarkBytesSink []byte + udpBenchmarkMessageSink Message +) + +func udpBenchmarkCases(payloadSize int) []udpBenchmarkCase { + content := bytes.Repeat([]byte{0x5a}, payloadSize) + return []udpBenchmarkCase{ + { + name: "ipv4-remote", + packet: &UDPPacket{ + Content: content, + RemoteAddr: &net.UDPAddr{IP: net.ParseIP("192.0.2.1"), Port: 12345}, + }, + }, + { + name: "ipv4-local-remote", + packet: &UDPPacket{ + Content: content, + LocalAddr: &net.UDPAddr{IP: net.ParseIP("192.0.2.2"), Port: 23456}, + RemoteAddr: &net.UDPAddr{IP: net.ParseIP("192.0.2.1"), Port: 12345}, + }, + }, + { + name: "ipv6-remote", + packet: &UDPPacket{ + Content: content, + RemoteAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345}, + }, + }, + { + name: "ipv6-local-remote", + packet: &UDPPacket{ + Content: content, + LocalAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::2"), Port: 23456, Zone: "bench0"}, + RemoteAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345, Zone: "bench1"}, + }, + }, + } +} + +func TestUDPPacketV2FrameSizes(t *testing.T) { + t.Logf("environment go=%s goos=%s goarch=%s gomaxprocs=%d", runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.GOMAXPROCS(0)) + for _, payloadSize := range []int{64, 512, 1200, 1472} { + for _, tc := range udpBenchmarkCases(payloadSize) { + jsonFrame := udpBenchmarkWireBytes(t, tc.packet, "") + binaryFrame := udpBenchmarkWireBytes(t, tc.packet, wire.UDPPacketCodecBinary) + saving := 100 * float64(len(jsonFrame)-len(binaryFrame)) / float64(len(jsonFrame)) + t.Logf("frame payload=%d case=%s json_bytes=%d binary_bytes=%d binary_saving_pct=%.2f", payloadSize, tc.name, len(jsonFrame), len(binaryFrame), saving) + } + } +} + +func udpBenchmarkWireBytes(t testing.TB, packet *UDPPacket, codec string) []byte { + t.Helper() + var buf bytes.Buffer + rw, err := NewUDPPacketReadWriter(&buf, wire.ProtocolV2, codec) + if err != nil { + t.Fatalf("create UDP read writer: %v", err) + } + if err := rw.WriteMsg(packet); err != nil { + t.Fatalf("write UDP packet: %v", err) + } + return append([]byte(nil), buf.Bytes()...) +} + +type udpBenchmarkReadWriter struct { + reader bytes.Reader +} + +func (rw *udpBenchmarkReadWriter) Read(p []byte) (int, error) { + return rw.reader.Read(p) +} + +func (rw *udpBenchmarkReadWriter) Write(p []byte) (int, error) { + return len(p), nil +} + +func (rw *udpBenchmarkReadWriter) Reset(p []byte) { + rw.reader.Reset(p) +} + +func udpBenchmarkValidatePacket(b testing.TB, got, want *UDPPacket) { + b.Helper() + if !bytes.Equal(got.Content, want.Content) || !udpBenchmarkUDPAddrEqual(got.LocalAddr, want.LocalAddr) || + !udpBenchmarkUDPAddrEqual(got.RemoteAddr, want.RemoteAddr) { + b.Fatalf("decoded packet mismatch: got %+v, want %+v", got, want) + } +} + +func udpBenchmarkUDPAddrEqual(got, want *net.UDPAddr) bool { + if got == nil || want == nil { + return got == want + } + return got.IP.Equal(want.IP) && got.Port == want.Port && got.Zone == want.Zone +} + +func BenchmarkUDPPacketV2CodecWrite(b *testing.B) { + for _, payloadSize := range []int{64, 512, 1200, 1472} { + for _, tc := range udpBenchmarkCases(payloadSize) { + for _, codec := range []struct { + name string + value string + }{ + {name: "json", value: ""}, + {name: "binary", value: wire.UDPPacketCodecBinary}, + } { + b.Run(fmt.Sprintf("payload-%d/%s/%s", payloadSize, tc.name, codec.name), func(b *testing.B) { + var buf bytes.Buffer + rw, err := NewUDPPacketReadWriter(&buf, wire.ProtocolV2, codec.value) + if err != nil { + b.Fatal(err) + } + expected := udpBenchmarkWireBytes(b, tc.packet, codec.value) + b.SetBytes(int64(len(expected))) + for b.Loop() { + buf.Reset() + if err := rw.WriteMsg(tc.packet); err != nil { + b.Fatal(err) + } + } + if !bytes.Equal(buf.Bytes(), expected) { + b.Fatalf("encoded packet mismatch: got %d bytes, want %d", buf.Len(), len(expected)) + } + udpBenchmarkBytesSink = buf.Bytes() + }) + } + } + } +} + +func BenchmarkUDPPacketV2CodecRead(b *testing.B) { + for _, payloadSize := range []int{64, 512, 1200, 1472} { + for _, tc := range udpBenchmarkCases(payloadSize) { + for _, codec := range []struct { + name string + value string + }{ + {name: "json", value: ""}, + {name: "binary", value: wire.UDPPacketCodecBinary}, + } { + b.Run(fmt.Sprintf("payload-%d/%s/%s", payloadSize, tc.name, codec.name), func(b *testing.B) { + encoded := udpBenchmarkWireBytes(b, tc.packet, codec.value) + stream := &udpBenchmarkReadWriter{} + rw, err := NewUDPPacketReadWriter(stream, wire.ProtocolV2, codec.value) + if err != nil { + b.Fatal(err) + } + var decoded Message + b.SetBytes(int64(len(encoded))) + for b.Loop() { + stream.Reset(encoded) + decoded, err = rw.ReadMsg() + if err != nil { + b.Fatal(err) + } + } + packet, ok := decoded.(*UDPPacket) + if !ok { + b.Fatalf("decoded message type %T, want *UDPPacket", decoded) + } + udpBenchmarkValidatePacket(b, packet, tc.packet) + udpBenchmarkMessageSink = decoded + }) + } + } + } +} diff --git a/server/control.go b/server/control.go index 1690b01b..8009d8ac 100644 --- a/server/control.go +++ b/server/control.go @@ -286,7 +286,7 @@ func (cm *ControlManager) GetByID(runID string) (ctl *Control, ok bool) { // admitVisitorByRunID commits a visitor admission against the current running // control while its run and lifecycle ownership are held. The callback must // only perform the in-memory, buffered visitor admission. -func (cm *ControlManager) admitVisitorByRunID(runID string, admit func(user string) error) (bool, error) { +func (cm *ControlManager) admitVisitorByRunID(runID string, admit func(user, wireProtocol, udpPacketCodec string) error) (bool, error) { entry, ok := cm.lockCurrentRun(runID, false) if !ok { return false, nil @@ -299,7 +299,7 @@ func (cm *ControlManager) admitVisitorByRunID(runID string, admit func(user stri if ctl.state != controlStateRunning { return false, nil } - return true, admit(ctl.sessionCtx.LoginMsg.User) + return true, admit(ctl.sessionCtx.LoginMsg.User, ctl.sessionCtx.WireProtocol, ctl.sessionCtx.UDPPacketCodec) } // RegisterWorkConn transfers conn to ctl only if ctl is still the current diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index 046794b6..e0dbb7f2 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -328,10 +328,18 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) { func (pxy *BaseProxy) joinUserConnection(local io.ReadWriteCloser, userConn net.Conn, proxyType string, xl *xlog.Logger) (int64, int64, []error) { visitorWireProtocol := wireProtocolFromConn(userConn) - if proxyType == string(v1.ProxyTypeSUDP) && isMixedWireProtocol(pxy.wireProtocol, visitorWireProtocol) { - xl.Infof("bridge mixed SUDP payload codecs, proxy wireProtocol [%s], visitor wireProtocol [%s]", - normalizeWireProtocol(pxy.wireProtocol), normalizeWireProtocol(visitorWireProtocol)) - return joinSUDPMessageBridge(local, userConn, pxy.wireProtocol, visitorWireProtocol, xl) + visitorUDPPacketCodec := udpPacketCodecFromConn(userConn) + if proxyType == string(v1.ProxyTypeSUDP) { + mixed, err := isMixedSUDPPacketEncoding(pxy.wireProtocol, pxy.udpPacketCodec, visitorWireProtocol, visitorUDPPacketCodec) + if err != nil { + return 0, 0, []error{err} + } + if mixed { + xl.Infof("bridge mixed SUDP payload codecs, proxy [%s/%s], visitor [%s/%s]", + normalizeWireProtocol(pxy.wireProtocol), pxy.udpPacketCodec, + normalizeWireProtocol(visitorWireProtocol), visitorUDPPacketCodec) + return joinSUDPMessageBridge(local, userConn, pxy.wireProtocol, pxy.udpPacketCodec, visitorWireProtocol, visitorUDPPacketCodec, xl) + } } return libio.Join(local, userConn) } @@ -340,6 +348,10 @@ type wireProtocolGetter interface { WireProtocol() string } +type udpPacketCodecGetter interface { + UDPPacketCodec() string +} + func wireProtocolFromConn(conn net.Conn) string { if getter, ok := conn.(wireProtocolGetter); ok { return getter.WireProtocol() @@ -347,10 +359,46 @@ func wireProtocolFromConn(conn net.Conn) string { return "" } +func udpPacketCodecFromConn(conn net.Conn) string { + if getter, ok := conn.(udpPacketCodecGetter); ok { + return getter.UDPPacketCodec() + } + return "" +} + func isMixedWireProtocol(left, right string) bool { return normalizeWireProtocol(left) != normalizeWireProtocol(right) } +func isMixedSUDPPacketEncoding(leftWire, leftCodec, rightWire, rightCodec string) (bool, error) { + leftCodec, err := normalizeUDPPacketCodec(leftWire, leftCodec) + if err != nil { + return false, fmt.Errorf("invalid left SUDP packet encoding: %w", err) + } + rightCodec, err = normalizeUDPPacketCodec(rightWire, rightCodec) + if err != nil { + return false, fmt.Errorf("invalid right SUDP packet encoding: %w", err) + } + return normalizeWireProtocol(leftWire) != normalizeWireProtocol(rightWire) || leftCodec != rightCodec, nil +} + +func normalizeUDPPacketCodec(wireProtocol, codec string) (string, error) { + switch wireProtocol { + case "", wire.ProtocolV1: + if codec != "" { + return "", fmt.Errorf("UDP packet codec %q requires wire protocol v2", codec) + } + return "", nil + case wire.ProtocolV2: + if codec == "" || codec == wire.UDPPacketCodecBinary { + return codec, nil + } + return "", fmt.Errorf("unsupported UDP packet codec %q", codec) + default: + return "", fmt.Errorf("unsupported wire protocol %q", wireProtocol) + } +} + func normalizeWireProtocol(wireProtocol string) string { if wireProtocol == wire.ProtocolV2 { return wire.ProtocolV2 @@ -362,13 +410,21 @@ func joinSUDPMessageBridge( proxyConn io.ReadWriteCloser, visitorConn io.ReadWriteCloser, proxyWireProtocol string, + proxyUDPPacketCodec string, visitorWireProtocol string, + visitorUDPPacketCodec string, xl *xlog.Logger, ) (inCount int64, outCount int64, errs []error) { // The mixed bridge decodes and re-encodes messages, so raw framed byte counts // are not available. Count UDP payload bytes and ignore heartbeat traffic. - proxyRW := msg.NewReadWriter(proxyConn, proxyWireProtocol) - visitorRW := msg.NewReadWriter(visitorConn, visitorWireProtocol) + proxyRW, err := msg.NewUDPPacketReadWriter(proxyConn, proxyWireProtocol, proxyUDPPacketCodec) + if err != nil { + return 0, 0, []error{err} + } + visitorRW, err := msg.NewUDPPacketReadWriter(visitorConn, visitorWireProtocol, visitorUDPPacketCodec) + if err != nil { + return 0, 0, []error{err} + } var ( once sync.Once diff --git a/server/proxy/sudp_benchmark_test.go b/server/proxy/sudp_benchmark_test.go new file mode 100644 index 00000000..a2e337d9 --- /dev/null +++ b/server/proxy/sudp_benchmark_test.go @@ -0,0 +1,232 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package proxy + +import ( + "bytes" + "fmt" + "io" + "net" + "testing" + + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/proto/wire" +) + +type sudpPathBenchmarkCase struct { + name string + packet *msg.UDPPacket +} + +var sudpPathBenchmarkBytesSink []byte + +func sudpPathBenchmarkCases(payloadSize int) []sudpPathBenchmarkCase { + content := bytes.Repeat([]byte{0x5a}, payloadSize) + return []sudpPathBenchmarkCase{ + { + name: "ipv4-remote", + packet: &msg.UDPPacket{ + Content: content, + RemoteAddr: &net.UDPAddr{ + IP: net.ParseIP("192.0.2.1"), Port: 12345, + }, + }, + }, + { + name: "ipv4-local-remote", + packet: &msg.UDPPacket{ + Content: content, + LocalAddr: &net.UDPAddr{ + IP: net.ParseIP("192.0.2.2"), Port: 23456, + }, + RemoteAddr: &net.UDPAddr{ + IP: net.ParseIP("192.0.2.1"), Port: 12345, + }, + }, + }, + { + name: "ipv6-remote", + packet: &msg.UDPPacket{ + Content: content, + RemoteAddr: &net.UDPAddr{ + IP: net.ParseIP("2001:db8::1"), Port: 12345, + }, + }, + }, + { + name: "ipv6-local-remote", + packet: &msg.UDPPacket{ + Content: content, + LocalAddr: &net.UDPAddr{ + IP: net.ParseIP("2001:db8::2"), Port: 23456, Zone: "bench0", + }, + RemoteAddr: &net.UDPAddr{ + IP: net.ParseIP("2001:db8::1"), Port: 12345, Zone: "bench1", + }, + }, + }, + } +} + +type sudpPathReadWriter struct { + reader bytes.Reader +} + +func (rw *sudpPathReadWriter) Read(p []byte) (int, error) { return rw.reader.Read(p) } +func (rw *sudpPathReadWriter) Write(p []byte) (int, error) { return len(p), nil } +func (rw *sudpPathReadWriter) Reset(p []byte) { rw.reader.Reset(p) } + +func sudpPathWireBytes(b testing.TB, packet *msg.UDPPacket, codec string) []byte { + b.Helper() + var buf bytes.Buffer + rw, err := msg.NewUDPPacketReadWriter(&buf, wire.ProtocolV2, codec) + if err != nil { + b.Fatal(err) + } + if err := rw.WriteMsg(packet); err != nil { + b.Fatal(err) + } + return append([]byte(nil), buf.Bytes()...) +} + +func sudpPathCopyFrame(dst, src []byte) []byte { + copy(dst, src) + return dst +} + +func BenchmarkSUDPInMemoryFrameCopy(b *testing.B) { + // This is an in-memory copy of an already encoded frame. It is a proxy for + // frame-size-dependent copy work, not a benchmark of libio.Join or sockets. + for _, payloadSize := range []int{64, 512, 1200, 1472} { + for _, tc := range sudpPathBenchmarkCases(payloadSize) { + for _, codec := range []struct { + name string + value string + }{ + {name: "json", value: ""}, + {name: "binary", value: wire.UDPPacketCodecBinary}, + } { + b.Run(fmt.Sprintf("payload-%d/%s/%s", payloadSize, tc.name, codec.name), func(b *testing.B) { + encoded := sudpPathWireBytes(b, tc.packet, codec.value) + dst := make([]byte, len(encoded)) + b.SetBytes(int64(len(encoded))) + for b.Loop() { + dst = sudpPathCopyFrame(dst, encoded) + } + if !bytes.Equal(dst, encoded) { + b.Fatal("copied frame does not match source") + } + sudpPathBenchmarkBytesSink = dst + }) + } + } + } +} + +func BenchmarkSUDPEndpointCodecPair(b *testing.B) { + // This measures an in-memory decode and re-encode with the same codec. It + // does not include the live SUDP server path, sockets, goroutines, or I/O. + for _, payloadSize := range []int{64, 512, 1200, 1472} { + for _, tc := range sudpPathBenchmarkCases(payloadSize) { + for _, codec := range []struct { + name string + value string + }{ + {name: "json", value: ""}, + {name: "binary", value: wire.UDPPacketCodecBinary}, + } { + b.Run(fmt.Sprintf("payload-%d/%s/%s", payloadSize, tc.name, codec.name), func(b *testing.B) { + encoded := sudpPathWireBytes(b, tc.packet, codec.value) + from := &sudpPathReadWriter{} + to := &bytes.Buffer{} + fromRW, err := msg.NewUDPPacketReadWriter(from, wire.ProtocolV2, codec.value) + if err != nil { + b.Fatal(err) + } + toRW, err := msg.NewUDPPacketReadWriter(to, wire.ProtocolV2, codec.value) + if err != nil { + b.Fatal(err) + } + b.SetBytes(int64(len(encoded))) + for b.Loop() { + from.Reset(encoded) + to.Reset() + m, err := fromRW.ReadMsg() + if err != nil { + b.Fatal(err) + } + if err := toRW.WriteMsg(m); err != nil { + b.Fatal(err) + } + } + if !bytes.Equal(to.Bytes(), encoded) { + b.Fatalf("re-encoded packet mismatch: got %d bytes, want %d", to.Len(), len(encoded)) + } + sudpPathBenchmarkBytesSink = to.Bytes() + }) + } + } + } +} + +func BenchmarkSUDPMixedCodecTranscodeModel(b *testing.B) { + // This exercises the real codec decode/re-encode pair used by the mixed + // bridge, excluding sockets, goroutines, crypto, compression, and framing I/O. + for _, payloadSize := range []int{64, 512, 1200, 1472} { + for _, tc := range sudpPathBenchmarkCases(payloadSize) { + for _, direction := range []struct { + name string + from string + to string + }{ + {name: "json-to-binary", from: "", to: wire.UDPPacketCodecBinary}, + {name: "binary-to-json", from: wire.UDPPacketCodecBinary, to: ""}, + } { + b.Run(fmt.Sprintf("payload-%d/%s/%s", payloadSize, tc.name, direction.name), func(b *testing.B) { + encoded := sudpPathWireBytes(b, tc.packet, direction.from) + expected := sudpPathWireBytes(b, tc.packet, direction.to) + from := &sudpPathReadWriter{} + to := &bytes.Buffer{} + fromRW, err := msg.NewUDPPacketReadWriter(from, wire.ProtocolV2, direction.from) + if err != nil { + b.Fatal(err) + } + toRW, err := msg.NewUDPPacketReadWriter(to, wire.ProtocolV2, direction.to) + if err != nil { + b.Fatal(err) + } + b.SetBytes(int64(len(encoded))) + for b.Loop() { + from.Reset(encoded) + to.Reset() + m, err := fromRW.ReadMsg() + if err != nil { + b.Fatal(err) + } + if err := toRW.WriteMsg(m); err != nil { + b.Fatal(err) + } + } + if !bytes.Equal(to.Bytes(), expected) { + b.Fatalf("transcoded packet mismatch: got %d bytes, want %d", to.Len(), len(expected)) + } + sudpPathBenchmarkBytesSink = to.Bytes() + }) + } + } + } +} + +var _ io.ReadWriter = (*sudpPathReadWriter)(nil) diff --git a/server/proxy/sudp_test.go b/server/proxy/sudp_test.go index e2986eed..b7ea9bc6 100644 --- a/server/proxy/sudp_test.go +++ b/server/proxy/sudp_test.go @@ -18,22 +18,27 @@ import ( "bufio" "bytes" "encoding/binary" + "io" + "net" "testing" + "time" "github.com/stretchr/testify/require" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/proto/wire" + "github.com/fatedier/frp/pkg/util/xlog" ) func TestSUDPBridgeTranscodesProxyV1ToVisitorV2(t *testing.T) { var in, out bytes.Buffer - writeSUDPBridgeMsg(t, &in, wire.ProtocolV1, &msg.UDPPacket{Content: []byte("proxy-to-visitor")}) + writeSUDPBridgeMsg(t, &in, wire.ProtocolV1, "", &msg.UDPPacket{Content: []byte("proxy-to-visitor")}) var count int64 err := bridgeSUDPProxyToVisitor( - msg.NewReadWriter(&in, wire.ProtocolV1), - msg.NewReadWriter(&out, wire.ProtocolV2), + newSUDPBridgeRW(t, &in, wire.ProtocolV1, ""), + newSUDPBridgeRW(t, &out, wire.ProtocolV2, ""), &count, nil, ) @@ -53,12 +58,12 @@ func TestSUDPBridgeTranscodesProxyV1ToVisitorV2(t *testing.T) { func TestSUDPBridgeTranscodesVisitorV2ToProxyV1(t *testing.T) { var in, out bytes.Buffer - writeSUDPBridgeMsg(t, &in, wire.ProtocolV2, &msg.UDPPacket{Content: []byte("visitor-to-proxy")}) + writeSUDPBridgeMsg(t, &in, wire.ProtocolV2, "", &msg.UDPPacket{Content: []byte("visitor-to-proxy")}) var count int64 err := bridgeSUDPVisitorToProxy( - msg.NewReadWriter(&in, wire.ProtocolV2), - msg.NewReadWriter(&out, wire.ProtocolV1), + newSUDPBridgeRW(t, &in, wire.ProtocolV2, ""), + newSUDPBridgeRW(t, &out, wire.ProtocolV1, ""), &count, nil, ) @@ -76,33 +81,67 @@ func TestSUDPBridgeTranscodesVisitorV2ToProxyV1(t *testing.T) { require.Equal(t, []byte("visitor-to-proxy"), got.Content) } -func TestSUDPBridgeForwardsProxyPing(t *testing.T) { +func TestSUDPBridgeTranscodesProxyV2BinaryToVisitorV2JSON(t *testing.T) { var in, out bytes.Buffer - writeSUDPBridgeMsg(t, &in, wire.ProtocolV1, &msg.Ping{}) + packet := newSUDPBridgeUDPPacket("proxy-binary-to-json") + writeSUDPBridgeMsg(t, &in, wire.ProtocolV2, wire.UDPPacketCodecBinary, packet) var count int64 err := bridgeSUDPProxyToVisitor( - msg.NewReadWriter(&in, wire.ProtocolV1), - msg.NewReadWriter(&out, wire.ProtocolV2), + newSUDPBridgeRW(t, &in, wire.ProtocolV2, wire.UDPPacketCodecBinary), + newSUDPBridgeRW(t, &out, wire.ProtocolV2, ""), + &count, + nil, + ) + require.NoError(t, err) + require.Equal(t, int64(len(packet.Content)), count) + requireV2UDPPacketFrame(t, &out, msg.V2TypeUDPPacket, packet) +} + +func TestSUDPBridgeTranscodesVisitorV2JSONToProxyV2Binary(t *testing.T) { + var in, out bytes.Buffer + packet := newSUDPBridgeUDPPacket("visitor-json-to-binary") + writeSUDPBridgeMsg(t, &in, wire.ProtocolV2, "", packet) + + var count int64 + err := bridgeSUDPVisitorToProxy( + newSUDPBridgeRW(t, &in, wire.ProtocolV2, ""), + newSUDPBridgeRW(t, &out, wire.ProtocolV2, wire.UDPPacketCodecBinary), + &count, + nil, + ) + require.NoError(t, err) + require.Equal(t, int64(len(packet.Content)), count) + requireV2UDPPacketFrame(t, &out, msg.V2TypeUDPPacketBinary, packet) +} + +func TestSUDPBridgeForwardsProxyPing(t *testing.T) { + var in, out bytes.Buffer + writeSUDPBridgeMsg(t, &in, wire.ProtocolV1, "", &msg.Ping{}) + + var count int64 + err := bridgeSUDPProxyToVisitor( + newSUDPBridgeRW(t, &in, wire.ProtocolV1, ""), + newSUDPBridgeRW(t, &out, wire.ProtocolV2, ""), &count, nil, ) require.NoError(t, err) require.Zero(t, count) - rawMsg, err := msg.NewReadWriter(&out, wire.ProtocolV2).ReadMsg() + rawMsg, err := newSUDPBridgeRW(t, &out, wire.ProtocolV2, "").ReadMsg() require.NoError(t, err) require.IsType(t, &msg.Ping{}, rawMsg) } func TestSUDPBridgeDropsVisitorPing(t *testing.T) { var in, out bytes.Buffer - writeSUDPBridgeMsg(t, &in, wire.ProtocolV2, &msg.Ping{}) + writeSUDPBridgeMsg(t, &in, wire.ProtocolV2, "", &msg.Ping{}) var count int64 err := bridgeSUDPVisitorToProxy( - msg.NewReadWriter(&in, wire.ProtocolV2), - msg.NewReadWriter(&out, wire.ProtocolV1), + newSUDPBridgeRW(t, &in, wire.ProtocolV2, ""), + newSUDPBridgeRW(t, &out, wire.ProtocolV1, ""), &count, nil, ) @@ -113,12 +152,12 @@ func TestSUDPBridgeDropsVisitorPing(t *testing.T) { func TestSUDPBridgeRejectsUnknownVisitorMessage(t *testing.T) { var in, out bytes.Buffer - writeSUDPBridgeMsg(t, &in, wire.ProtocolV2, &msg.Pong{}) + writeSUDPBridgeMsg(t, &in, wire.ProtocolV2, "", &msg.Pong{}) var count int64 err := bridgeSUDPVisitorToProxy( - msg.NewReadWriter(&in, wire.ProtocolV2), - msg.NewReadWriter(&out, wire.ProtocolV1), + newSUDPBridgeRW(t, &in, wire.ProtocolV2, ""), + newSUDPBridgeRW(t, &out, wire.ProtocolV1, ""), &count, nil, ) @@ -127,6 +166,22 @@ func TestSUDPBridgeRejectsUnknownVisitorMessage(t *testing.T) { require.Empty(t, out.Bytes()) } +func TestSUDPBridgeRejectsMismatchedPacketCodecOnStream(t *testing.T) { + var in, out bytes.Buffer + writeSUDPBridgeMsg(t, &in, wire.ProtocolV2, "", newSUDPBridgeUDPPacket("json-on-binary-stream")) + + var count int64 + err := bridgeSUDPProxyToVisitor( + newSUDPBridgeRW(t, &in, wire.ProtocolV2, wire.UDPPacketCodecBinary), + newSUDPBridgeRW(t, &out, wire.ProtocolV2, ""), + &count, + nil, + ) + require.ErrorContains(t, err, "received JSON UDP packet after binary codec negotiation") + require.Zero(t, count) + require.Empty(t, out.Bytes()) +} + func TestSUDPBridgeDetectsMixedWireProtocol(t *testing.T) { require.False(t, isMixedWireProtocol("", wire.ProtocolV1)) require.False(t, isMixedWireProtocol(wire.ProtocolV2, wire.ProtocolV2)) @@ -134,8 +189,170 @@ func TestSUDPBridgeDetectsMixedWireProtocol(t *testing.T) { require.True(t, isMixedWireProtocol(wire.ProtocolV2, wire.ProtocolV1)) } -func writeSUDPBridgeMsg(t *testing.T, buf *bytes.Buffer, wireProtocol string, m msg.Message) { - t.Helper() - - require.NoError(t, msg.NewReadWriter(buf, wireProtocol).WriteMsg(m)) +func TestSUDPBridgeDetectsMixedPacketEncoding(t *testing.T) { + for _, tc := range []struct { + name string + leftWire string + leftCodec string + rightWire string + rightCodec string + mixed bool + }{ + {name: "legacy v1 aliases explicit v1", leftWire: "", rightWire: wire.ProtocolV1}, + {name: "v2 json matches v2 json", leftWire: wire.ProtocolV2, rightWire: wire.ProtocolV2}, + { + name: "v2 binary matches v2 binary", + leftWire: wire.ProtocolV2, + leftCodec: wire.UDPPacketCodecBinary, + rightWire: wire.ProtocolV2, + rightCodec: wire.UDPPacketCodecBinary, + }, + {name: "v1 json differs from v2 json", leftWire: wire.ProtocolV1, rightWire: wire.ProtocolV2, mixed: true}, + { + name: "v2 json differs from v2 binary", + leftWire: wire.ProtocolV2, + rightWire: wire.ProtocolV2, + rightCodec: wire.UDPPacketCodecBinary, + mixed: true, + }, + { + name: "v2 binary differs from v1 json", + leftWire: wire.ProtocolV2, + leftCodec: wire.UDPPacketCodecBinary, + rightWire: wire.ProtocolV1, + mixed: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + mixed, err := isMixedSUDPPacketEncoding(tc.leftWire, tc.leftCodec, tc.rightWire, tc.rightCodec) + require.NoError(t, err) + require.Equal(t, tc.mixed, mixed) + }) + } +} + +func TestSUDPBridgeRejectsInvalidEncodingMetadata(t *testing.T) { + for _, tc := range []struct { + name string + leftWire string + leftCodec string + rightWire string + rightCodec string + wantErr string + }{ + { + name: "left v1 binary", + leftWire: wire.ProtocolV1, + leftCodec: wire.UDPPacketCodecBinary, + rightWire: wire.ProtocolV1, + wantErr: "invalid left SUDP packet encoding", + }, + { + name: "right unknown v2 codec", + leftWire: wire.ProtocolV2, + rightWire: wire.ProtocolV2, + rightCodec: "snappy", + wantErr: "invalid right SUDP packet encoding", + }, + {name: "left unknown wire", leftWire: "v3", rightWire: wire.ProtocolV2, wantErr: "unsupported wire protocol"}, + } { + t.Run(tc.name, func(t *testing.T) { + mixed, err := isMixedSUDPPacketEncoding(tc.leftWire, tc.leftCodec, tc.rightWire, tc.rightCodec) + require.False(t, mixed) + require.ErrorContains(t, err, tc.wantErr) + }) + } +} + +func TestSUDPJoinUsesRawPathForSameEncodingState(t *testing.T) { + proxyClient, proxyServer := net.Pipe() + visitorClient, visitorServer := net.Pipe() + t.Cleanup(func() { + _ = proxyClient.Close() + _ = proxyServer.Close() + _ = visitorClient.Close() + _ = visitorServer.Close() + }) + deadline := time.Now().Add(3 * time.Second) + require.NoError(t, proxyClient.SetDeadline(deadline)) + require.NoError(t, proxyServer.SetDeadline(deadline)) + require.NoError(t, visitorClient.SetDeadline(deadline)) + require.NoError(t, visitorServer.SetDeadline(deadline)) + + pxy := &BaseProxy{ + configurer: &v1.SUDPProxyConfig{}, + wireProtocol: wire.ProtocolV2, + udpPacketCodec: wire.UDPPacketCodecBinary, + } + visitorConn := &metadataConn{Conn: visitorServer, wireProtocol: wire.ProtocolV2, udpPacketCodec: wire.UDPPacketCodecBinary} + joinDone := make(chan []error, 1) + go func() { + _, _, errs := pxy.joinUserConnection(proxyServer, visitorConn, string(v1.ProxyTypeSUDP), xlog.New()) + joinDone <- errs + }() + + raw := []byte{0, 16, 0, 0, 0, 4, 0xde, 0xad, 0xbe, 0xef} + _, err := proxyClient.Write(raw) + require.NoError(t, err) + got := make([]byte, len(raw)) + _, err = io.ReadFull(visitorClient, got) + require.NoError(t, err) + require.Equal(t, raw, got) + + _ = proxyClient.Close() + _ = visitorClient.Close() + <-joinDone +} + +func newSUDPBridgeRW(t *testing.T, buf *bytes.Buffer, wireProtocol, udpPacketCodec string) msg.ReadWriter { + t.Helper() + rw, err := msg.NewUDPPacketReadWriter(buf, wireProtocol, udpPacketCodec) + require.NoError(t, err) + return rw +} + +func writeSUDPBridgeMsg(t *testing.T, buf *bytes.Buffer, wireProtocol, udpPacketCodec string, m msg.Message) { + t.Helper() + require.NoError(t, newSUDPBridgeRW(t, buf, wireProtocol, udpPacketCodec).WriteMsg(m)) +} + +func newSUDPBridgeUDPPacket(content string) *msg.UDPPacket { + return &msg.UDPPacket{ + Content: []byte(content), + RemoteAddr: &net.UDPAddr{IP: net.ParseIP("192.0.2.1"), Port: 12345}, + } +} + +func requireV2UDPPacketFrame(t *testing.T, buf *bytes.Buffer, wantType uint16, want *msg.UDPPacket) { + t.Helper() + frame, err := wire.NewConn(buf).ReadFrame() + require.NoError(t, err) + require.Equal(t, wire.FrameTypeMessage, frame.Type) + require.GreaterOrEqual(t, len(frame.Payload), 2) + require.Equal(t, wantType, binary.BigEndian.Uint16(frame.Payload[:2])) + var got *msg.UDPPacket + if wantType == msg.V2TypeUDPPacketBinary { + got, err = msg.DecodeUDPPacketBinary(frame.Payload[2:]) + } else { + var decoded msg.UDPPacket + err = msg.DecodeV2MessageFrameInto(frame, &decoded) + got = &decoded + } + require.NoError(t, err) + require.Equal(t, want.Content, got.Content) + require.Equal(t, want.RemoteAddr.String(), got.RemoteAddr.String()) +} + +type metadataConn struct { + net.Conn + wireProtocol string + udpPacketCodec string +} + +func (c *metadataConn) WireProtocol() string { + return c.wireProtocol +} + +func (c *metadataConn) UDPPacketCodec() string { + return c.udpPacketCodec } diff --git a/server/service.go b/server/service.go index e24acf66..7aba085e 100644 --- a/server/service.go +++ b/server/service.go @@ -885,14 +885,22 @@ func (svr *Service) RegisterWorkConn( } func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVisitorConn, wireProtocol string) error { - admit := func(visitorUser string) error { + admit := func(visitorUser, visitorWireProtocol, visitorUDPPacketCodec string) error { + if visitorWireProtocol == "" { + visitorWireProtocol = wireProtocol + } return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey, - newMsg.UseEncryption, newMsg.UseCompression, visitorUser, wireProtocol) + newMsg.UseEncryption, newMsg.UseCompression, visitorUser, visitorWireProtocol, visitorUDPPacketCodec) } // TODO(deprecation): Compatible with old versions, can be without runID, user is empty. In later versions, it will be mandatory to include runID. // If runID is required, it is not compatible with versions prior to v0.50.0. if newMsg.RunID != "" { - admitted, err := svr.ctlManager.admitVisitorByRunID(newMsg.RunID, admit) + admitted, err := svr.ctlManager.admitVisitorByRunID(newMsg.RunID, func(visitorUser, controlWireProtocol, controlUDPPacketCodec string) error { + if wireProtocol != controlWireProtocol { + return fmt.Errorf("visitor connection wire protocol mismatch: got %s want %s", wireProtocol, controlWireProtocol) + } + return admit(visitorUser, controlWireProtocol, controlUDPPacketCodec) + }) if err != nil { return err } @@ -901,5 +909,5 @@ func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVis } return nil } - return admit("") + return admit("", wireProtocol, "") } diff --git a/server/service_test.go b/server/service_test.go index cc7645f8..e35f11b9 100644 --- a/server/service_test.go +++ b/server/service_test.go @@ -428,20 +428,24 @@ func TestServiceVisitorAdmissionSerializesReplacement(t *testing.T) { } t.Cleanup(resume) type admissionResult struct { - admitted bool - user string - err error + admitted bool + user string + wireProtocol string + udpPacketCodec string + err error } admissionDone := make(chan admissionResult, 1) go func() { - var admittedUser string - admitted, admitErr := svr.ctlManager.admitVisitorByRunID("shared-run", func(user string) error { - admittedUser = user + result := admissionResult{} + result.admitted, result.err = svr.ctlManager.admitVisitorByRunID("shared-run", func(user, wireProtocol, udpPacketCodec string) error { + result.user = user + result.wireProtocol = wireProtocol + result.udpPacketCodec = udpPacketCodec close(admissionEntered) <-resumeAdmission return nil }) - admissionDone <- admissionResult{admitted: admitted, user: admittedUser, err: admitErr} + admissionDone <- result }() waitForSignal(t, admissionEntered, "visitor admission callback") runMu := currentRunGateForTest(svr.ctlManager, "shared-run") @@ -477,6 +481,8 @@ func TestServiceVisitorAdmissionSerializesReplacement(t *testing.T) { require.NoError(t, admission.err) require.True(t, admission.admitted) require.Equal(t, "old-user", admission.user) + require.Equal(t, wire.ProtocolV1, admission.wireProtocol) + require.Empty(t, admission.udpPacketCodec) replacement := waitForResult(t, replacementDone, "replacement") require.NoError(t, replacement.err) ctlB := replacement.ctl @@ -762,6 +768,81 @@ func TestServiceVisitorRoutingExcludesPendingUser(t *testing.T) { waitForControlDone(t, ctl) } +func TestServiceVisitorRoutingCarriesControlPacketCodec(t *testing.T) { + svr := newControlTestService(t) + listener, err := svr.rc.VisitorManager.Listen("visitor", "secret", []string{"visitor-user"}) + require.NoError(t, err) + t.Cleanup(func() { _ = listener.Close() }) + + controlConn := newDeadlineReadConn() + controlMsgConn := msg.NewConn(controlConn, msg.NewV2ReadWriter(controlConn)) + ctl, err := svr.RegisterControl(controlMsgConn, &msg.Login{ + RunID: "visitor-binary-run", + User: "visitor-user", + ClientID: "visitor-client", + ClientSpec: msg.ClientSpec{ + AlwaysAuthPass: true, + }, + }, true, wire.ProtocolV2, wire.UDPPacketCodecBinary) + require.NoError(t, err) + + timestamp := time.Now().Unix() + visitorMsg := &msg.NewVisitorConn{ + RunID: "visitor-binary-run", + ProxyName: "visitor", + Timestamp: timestamp, + SignKey: util.GetAuthKey("secret", timestamp), + } + require.NoError(t, svr.completeControlLogin(ctl, func() error { return nil })) + waitForSignal(t, controlConn.readStarted, "binary visitor control reader to start") + + runningConn := newCountingCloseConn() + require.NoError(t, svr.RegisterVisitorConn(runningConn, visitorMsg, wire.ProtocolV2)) + accepted, err := listener.Accept() + require.NoError(t, err) + metadata, ok := accepted.(interface { + WireProtocol() string + UDPPacketCodec() string + }) + require.True(t, ok) + require.Equal(t, wire.ProtocolV2, metadata.WireProtocol()) + require.Equal(t, wire.UDPPacketCodecBinary, metadata.UDPPacketCodec()) + require.NoError(t, accepted.Close()) + require.Equal(t, int64(1), runningConn.closeCount.Load()) + + mismatchConn := newCountingCloseConn() + err = svr.RegisterVisitorConn(mismatchConn, visitorMsg, wire.ProtocolV1) + require.ErrorContains(t, err, "visitor connection wire protocol mismatch") + require.NoError(t, mismatchConn.Close()) + require.Equal(t, int64(1), mismatchConn.closeCount.Load()) + + require.NoError(t, ctl.Close()) + waitForControlDone(t, ctl) +} + +func TestServiceVisitorRoutingLegacyFallsBackToJSONPacketCodec(t *testing.T) { + svr := newControlTestService(t) + listener, err := svr.rc.VisitorManager.Listen("visitor", "secret", []string{""}) + require.NoError(t, err) + t.Cleanup(func() { _ = listener.Close() }) + + timestamp := time.Now().Unix() + visitorMsg := &msg.NewVisitorConn{ + ProxyName: "visitor", + Timestamp: timestamp, + SignKey: util.GetAuthKey("secret", timestamp), + } + visitorConn := newCountingCloseConn() + require.NoError(t, svr.RegisterVisitorConn(visitorConn, visitorMsg, wire.ProtocolV2)) + accepted, err := listener.Accept() + require.NoError(t, err) + metadata, ok := accepted.(interface{ UDPPacketCodec() string }) + require.True(t, ok) + require.Empty(t, metadata.UDPPacketCodec()) + require.NoError(t, accepted.Close()) + require.Equal(t, int64(1), visitorConn.closeCount.Load()) +} + func newControlTestService(t *testing.T) *Service { t.Helper() cfg := &v1.ServerConfig{} diff --git a/server/visitor/visitor.go b/server/visitor/visitor.go index 11a44367..7776aad8 100644 --- a/server/visitor/visitor.go +++ b/server/visitor/visitor.go @@ -65,8 +65,12 @@ func (vm *Manager) Listen(name string, sk string, allowUsers []string) (*netpkg. func (vm *Manager) NewConn(name string, conn net.Conn, timestamp int64, signKey string, useEncryption bool, useCompression bool, visitorUser string, - wireProtocol string, + wireProtocol string, udpPacketCodecs ...string, ) (err error) { + udpPacketCodec := "" + if len(udpPacketCodecs) > 0 { + udpPacketCodec = udpPacketCodecs[0] + } vm.mu.RLock() defer vm.mu.RUnlock() @@ -93,8 +97,9 @@ func (vm *Manager) NewConn(name string, conn net.Conn, timestamp int64, signKey } visitorConn := netpkg.WrapReadWriteCloserToConn(rwc, conn) err = l.l.PutConn(&wireProtocolConn{ - Conn: visitorConn, - wireProtocol: wireProtocol, + Conn: visitorConn, + wireProtocol: wireProtocol, + udpPacketCodec: udpPacketCodec, }) } else { err = fmt.Errorf("custom listener for [%s] doesn't exist", name) @@ -105,13 +110,18 @@ func (vm *Manager) NewConn(name string, conn net.Conn, timestamp int64, signKey type wireProtocolConn struct { net.Conn - wireProtocol string + wireProtocol string + udpPacketCodec string } func (c *wireProtocolConn) WireProtocol() string { return c.wireProtocol } +func (c *wireProtocolConn) UDPPacketCodec() string { + return c.udpPacketCodec +} + func (vm *Manager) CloseListener(name string) { vm.mu.Lock() defer vm.mu.Unlock() diff --git a/server/visitor/visitor_test.go b/server/visitor/visitor_test.go index 67b907fd..04c50ff0 100644 --- a/server/visitor/visitor_test.go +++ b/server/visitor/visitor_test.go @@ -25,7 +25,7 @@ import ( "github.com/fatedier/frp/pkg/util/util" ) -func TestManagerNewConnCarriesWireProtocol(t *testing.T) { +func TestManagerNewConnCarriesWireProtocolAndUDPPacketCodec(t *testing.T) { vm := NewManager() listener, err := vm.Listen("sudp", "secret", []string{"*"}) require.NoError(t, err) @@ -47,6 +47,7 @@ func TestManagerNewConnCarriesWireProtocol(t *testing.T) { false, "user", wire.ProtocolV2, + wire.UDPPacketCodecBinary, ) }() @@ -54,8 +55,12 @@ func TestManagerNewConnCarriesWireProtocol(t *testing.T) { require.NoError(t, err) defer acceptedConn.Close() - getter, ok := acceptedConn.(interface{ WireProtocol() string }) + metadata, ok := acceptedConn.(interface { + WireProtocol() string + UDPPacketCodec() string + }) require.True(t, ok) - require.Equal(t, wire.ProtocolV2, getter.WireProtocol()) + require.Equal(t, wire.ProtocolV2, metadata.WireProtocol()) + require.Equal(t, wire.UDPPacketCodecBinary, metadata.UDPPacketCodec()) require.NoError(t, <-errCh) } diff --git a/test/e2e/v1/basic/wire.go b/test/e2e/v1/basic/wire.go index cb61558f..bf475287 100644 --- a/test/e2e/v1/basic/wire.go +++ b/test/e2e/v1/basic/wire.go @@ -110,12 +110,12 @@ var _ = ginkgo.Describe("[Feature: WireProtocol]", func() { name: "default sudp visitor", }, { - name: "v2 sudp visitor", + name: "v2 binary raw sudp visitor", proxyWireConfig: `transport.wireProtocol = "v2"`, visitorWireConfig: `transport.wireProtocol = "v2"`, }, { - name: "mixed sudp proxy v1 visitor v2", + name: "v1 JSON proxy -> v2 Binary visitor transcode", proxyWireConfig: `transport.wireProtocol = "v1"`, visitorWireConfig: `transport.wireProtocol = "v2"`, extraProxyConfig: ` @@ -128,7 +128,7 @@ var _ = ginkgo.Describe("[Feature: WireProtocol]", func() { `, }, { - name: "mixed sudp proxy v2 visitor v1", + name: "v2 Binary proxy -> v1 JSON visitor transcode", proxyWireConfig: `transport.wireProtocol = "v2"`, visitorWireConfig: `transport.wireProtocol = "v1"`, }, From 290017cb53671fd1e838380104b561d58fd116a8 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 3 Aug 2026 23:37:36 +0800 Subject: [PATCH 13/23] update version to 0.71.0 (#5464) --- README.md | 20 ++++++++++---------- README_zh.md | 20 ++++++++++---------- pkg/util/version/version.go | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 8dad3b44..c3bc2f60 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,16 @@ frp is an open source project with its ongoing development made possible entirel

Gold Sponsors

+

+ + +
+ The sovereign cloud that puts you in control +
+ An open source, self-hosted alternative to public clouds, built for data ownership and privacy +
+

+
## Recall.ai - API for meeting recordings @@ -29,16 +39,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and The complete IDE crafted for professional Go developers

- -

- - -
- The sovereign cloud that puts you in control -
- An open source, self-hosted alternative to public clouds, built for data ownership and privacy -
-

## What is frp? diff --git a/README_zh.md b/README_zh.md index 713c7b57..0f0da0eb 100644 --- a/README_zh.md +++ b/README_zh.md @@ -15,6 +15,16 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者

Gold Sponsors

+

+ + +
+ The sovereign cloud that puts you in control +
+ An open source, self-hosted alternative to public clouds, built for data ownership and privacy +
+

+
## Recall.ai - API for meeting recordings @@ -32,16 +42,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and The complete IDE crafted for professional Go developers

- -

- - -
- The sovereign cloud that puts you in control -
- An open source, self-hosted alternative to public clouds, built for data ownership and privacy -
-

## 为什么使用 frp ? diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index ca5ec108..5c232712 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.70.1" +var version = "0.71.0" func Full() string { return version From 1a3a872bd2162116702012124b6e948c6c02af7a Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 4 Aug 2026 12:53:10 +0800 Subject: [PATCH 14/23] fix(frpc): respect feature gates in verify (#5465) --- Release.md | 1 + client/config_manager_test.go | 254 ++++++++++++++++++++++++ client/service.go | 8 + cmd/frpc/sub/root.go | 7 - cmd/frpc/sub/verify.go | 19 +- cmd/frpc/sub/verify_test.go | 67 +++++++ pkg/config/v1/validation/client.go | 39 +++- pkg/config/v1/validation/client_test.go | 140 +++++++++++++ 8 files changed, 521 insertions(+), 14 deletions(-) create mode 100644 cmd/frpc/sub/verify_test.go create mode 100644 pkg/config/v1/validation/client_test.go diff --git a/Release.md b/Release.md index 0d2500d6..4716e374 100644 --- a/Release.md +++ b/Release.md @@ -5,3 +5,4 @@ ## Fixes * Fixed a server panic and remote denial of service caused by a client sending a negative `pool_count`. Negative values are now rejected before work-connection pool resources are allocated. +* Fixed `frpc verify` ignoring configured `featureGates`, which caused VirtualNet configurations to be rejected even when the feature was enabled. diff --git a/client/config_manager_test.go b/client/config_manager_test.go index 07ae3297..2758b2d7 100644 --- a/client/config_manager_test.go +++ b/client/config_manager_test.go @@ -2,12 +2,16 @@ package client import ( "errors" + "os" "path/filepath" + "strings" "testing" "github.com/fatedier/frp/client/configmgmt" "github.com/fatedier/frp/pkg/config/source" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/policy/security" + "github.com/fatedier/frp/pkg/vnet" ) func newTestRawTCPProxyConfig(name string) *v1.TCPProxyConfig { @@ -22,6 +26,256 @@ func newTestRawTCPProxyConfig(name string) *v1.TCPProxyConfig { } } +func newTestVirtualNetProxyConfig(name string) *v1.STCPProxyConfig { + return &v1.STCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: name, + Type: "stcp", + ProxyBackend: v1.ProxyBackend{ + Plugin: v1.TypedClientPluginOptions{ + Type: v1.PluginVirtualNet, + ClientPluginOptions: &v1.VirtualNetPluginOptions{Type: v1.PluginVirtualNet}, + }, + }, + }, + } +} + +func newTestVirtualNetVisitorConfig(name string) *v1.STCPVisitorConfig { + return &v1.STCPVisitorConfig{ + VisitorBaseConfig: v1.VisitorBaseConfig{ + Name: name, + Type: "stcp", + ServerName: "vnet-server", + SecretKey: "secret", + BindPort: -1, + Plugin: v1.TypedVisitorPluginOptions{ + Type: v1.VisitorPluginVirtualNet, + VisitorPluginOptions: &v1.VirtualNetVisitorPluginOptions{ + Type: v1.VisitorPluginVirtualNet, + DestinationIP: "100.86.0.1", + }, + }, + }, + } +} + +func TestServiceConfigManagerReloadVirtualNetRuntimeDependency(t *testing.T) { + const runtimeErr = "VirtualNet-dependent configuration requires a VirtualNet runtime enabled at startup" + + tests := []struct { + name string + startupVirtualNetAddr string + nextConfig string + wantRuntimeDependency bool + }{ + { + name: "unrelated common config", + nextConfig: `serverAddr = "0.0.0.0"`, + }, + { + name: "VirtualNet address without startup runtime", + nextConfig: `featureGates = { VirtualNet = true } +virtualNet.address = "100.86.0.4/24" +`, + wantRuntimeDependency: true, + }, + { + name: "VirtualNet proxy without startup runtime", + nextConfig: `[[proxies]] +name = "vnet-proxy" +type = "stcp" +secretKey = "secret" +[proxies.plugin] +type = "virtual_net" +`, + wantRuntimeDependency: true, + }, + { + name: "VirtualNet visitor without startup runtime", + nextConfig: `[[visitors]] +name = "vnet-visitor" +type = "stcp" +serverName = "vnet-server" +secretKey = "secret" +bindPort = -1 +[visitors.plugin] +type = "virtual_net" +destinationIP = "100.86.0.1" +`, + wantRuntimeDependency: true, + }, + { + name: "existing VirtualNet startup runtime", + startupVirtualNetAddr: "100.86.0.4/24", + nextConfig: `featureGates = { VirtualNet = true } +virtualNet.address = "100.86.0.5/24" +`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + current := &v1.ClientCommonConfig{} + if tc.startupVirtualNetAddr != "" { + current.FeatureGates = map[string]bool{"VirtualNet": true} + current.VirtualNet.Address = tc.startupVirtualNetAddr + } + if err := current.Complete(); err != nil { + t.Fatalf("complete current config: %v", err) + } + + configFile := filepath.Join(t.TempDir(), "frpc.toml") + if err := os.WriteFile(configFile, []byte(tc.nextConfig), 0o600); err != nil { + t.Fatalf("write config: %v", err) + } + + configSource := source.NewConfigSource() + aggregator := source.NewAggregator(configSource) + svr := &Service{ + common: current, + reloadCommon: current, + configFilePath: configFile, + unsafeFeatures: security.NewUnsafeFeatures(nil), + aggregator: aggregator, + configSource: configSource, + } + if tc.startupVirtualNetAddr != "" { + svr.vnetController = vnet.NewController(current.VirtualNet) + } + + err := (&serviceConfigManager{svr: svr}).ReloadFromFile(true) + if tc.wantRuntimeDependency { + if !errors.Is(err, configmgmt.ErrApplyConfig) || !strings.Contains(err.Error(), runtimeErr) { + t.Fatalf("expected VirtualNet runtime dependency error, got: %v", err) + } + return + } + + if err != nil { + t.Fatalf("reload config: %v", err) + } + if svr.common != current { + t.Fatal("reload should not replace startup common config") + } + if tc.startupVirtualNetAddr == "" && svr.vnetController != nil { + t.Fatal("reload should not enable startup-only VirtualNet runtime state") + } + }) + } +} + +func TestServiceConfigManagerReloadVirtualNetRuntimeDependencyUsesMergedSources(t *testing.T) { + const runtimeErr = "VirtualNet-dependent configuration requires a VirtualNet runtime enabled at startup" + + tests := []struct { + name string + nextConfig string + storeProxy v1.ProxyConfigurer + storeVisitor v1.VisitorConfigurer + wantRuntimeDependency bool + wantProxyPlugin string + }{ + { + name: "Store VirtualNet proxy is rejected", + nextConfig: `serverAddr = "0.0.0.0"`, + storeProxy: newTestVirtualNetProxyConfig("store-vnet"), + wantRuntimeDependency: true, + }, + { + name: "Store VirtualNet visitor is rejected", + nextConfig: `serverAddr = "0.0.0.0"`, + storeVisitor: newTestVirtualNetVisitorConfig("store-vnet"), + wantRuntimeDependency: true, + }, + { + name: "Store VirtualNet proxy overrides file proxy", + nextConfig: `[[proxies]] +name = "shared" +type = "tcp" +localPort = 10080 +remotePort = 10081 +`, + storeProxy: newTestVirtualNetProxyConfig("shared"), + wantRuntimeDependency: true, + }, + { + name: "Store non-VirtualNet proxy overrides file VirtualNet proxy", + nextConfig: `[[proxies]] +name = "shared" +type = "stcp" +secretKey = "secret" +[proxies.plugin] +type = "virtual_net" +`, + storeProxy: newTestRawTCPProxyConfig("shared"), + wantProxyPlugin: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + current := &v1.ClientCommonConfig{} + if err := current.Complete(); err != nil { + t.Fatalf("complete current config: %v", err) + } + + configFile := filepath.Join(t.TempDir(), "frpc.toml") + if err := os.WriteFile(configFile, []byte(tc.nextConfig), 0o600); err != nil { + t.Fatalf("write config: %v", err) + } + + storeSource, err := source.NewStoreSource(source.StoreSourceConfig{ + Path: filepath.Join(t.TempDir(), "store.json"), + }) + if err != nil { + t.Fatalf("new store source: %v", err) + } + if tc.storeProxy != nil { + if err := storeSource.AddProxy(tc.storeProxy); err != nil { + t.Fatalf("add store proxy: %v", err) + } + } + if tc.storeVisitor != nil { + if err := storeSource.AddVisitor(tc.storeVisitor); err != nil { + t.Fatalf("add store visitor: %v", err) + } + } + + configSource := source.NewConfigSource() + aggregator := source.NewAggregator(configSource) + aggregator.SetStoreSource(storeSource) + svr := &Service{ + common: current, + reloadCommon: current, + configFilePath: configFile, + unsafeFeatures: security.NewUnsafeFeatures(nil), + aggregator: aggregator, + configSource: configSource, + storeSource: storeSource, + } + + err = (&serviceConfigManager{svr: svr}).ReloadFromFile(true) + if tc.wantRuntimeDependency { + if !errors.Is(err, configmgmt.ErrApplyConfig) || !strings.Contains(err.Error(), runtimeErr) { + t.Fatalf("expected VirtualNet runtime dependency error, got: %v", err) + } + return + } + if err != nil { + t.Fatalf("reload config: %v", err) + } + + if len(svr.proxyCfgs) != 1 { + t.Fatalf("expected one applied proxy, got %d", len(svr.proxyCfgs)) + } + if got := svr.proxyCfgs[0].GetBaseConfig().Plugin.Type; got != tc.wantProxyPlugin { + t.Fatalf("unexpected applied proxy plugin: %q", got) + } + }) + } +} + func TestServiceConfigManagerCreateStoreProxyConflict(t *testing.T) { storeSource, err := source.NewStoreSource(source.StoreSourceConfig{ Path: filepath.Join(t.TempDir(), "store.json"), diff --git a/client/service.go b/client/service.go index 40428719..7964050f 100644 --- a/client/service.go +++ b/client/service.go @@ -33,6 +33,7 @@ import ( "github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config/source" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/policy/security" httppkg "github.com/fatedier/frp/pkg/util/http" @@ -510,6 +511,13 @@ func (svr *Service) reloadConfigFromSourcesLocked() error { proxies, visitors = config.FilterClientConfigurers(reloadCommon, proxies, visitors) proxies = config.CompleteProxyConfigurers(proxies) visitors = config.CompleteVisitorConfigurers(visitors) + requirements := validation.GetClientConfigRequirements(reloadCommon, proxies, visitors) + if svr.vnetController == nil && requirements.VirtualNet { + return errors.New( + "VirtualNet-dependent configuration requires a VirtualNet runtime enabled at startup; " + + "restart frpc after configuring featureGates.VirtualNet and virtualNet.address", + ) + } // Atomically replace the entire configuration if err := svr.UpdateAllConfigurer(proxies, visitors); err != nil { diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 5d83cabf..8a0f1842 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -33,7 +33,6 @@ import ( "github.com/fatedier/frp/pkg/config/source" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/config/v1/validation" - "github.com/fatedier/frp/pkg/policy/featuregate" "github.com/fatedier/frp/pkg/policy/security" "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/version" @@ -131,12 +130,6 @@ func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) erro "please use yaml/json/toml format instead!\n") } - if len(result.Common.FeatureGates) > 0 { - if err := featuregate.SetFromMap(result.Common.FeatureGates); err != nil { - return err - } - } - return runClientWithAggregator(result, unsafeFeatures, cfgFilePath) } diff --git a/cmd/frpc/sub/verify.go b/cmd/frpc/sub/verify.go index 830f7bf1..d226c473 100644 --- a/cmd/frpc/sub/verify.go +++ b/cmd/frpc/sub/verify.go @@ -29,6 +29,18 @@ func init() { rootCmd.AddCommand(verifyCmd) } +func verifyClientConfig( + configFile string, + strict bool, + unsafeFeatures *security.UnsafeFeatures, +) (validation.Warning, error) { + cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(configFile, strict) + if err != nil { + return nil, err + } + return validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures) +} + var verifyCmd = &cobra.Command{ Use: "verify", Short: "Verify that the configures is valid", @@ -38,13 +50,8 @@ var verifyCmd = &cobra.Command{ return nil } - cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) - if err != nil { - fmt.Println(err) - os.Exit(1) - } unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) - warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures) + warning, err := verifyClientConfig(cfgFile, strictConfigMode, unsafeFeatures) if warning != nil { fmt.Printf("WARNING: %v\n", warning) } diff --git a/cmd/frpc/sub/verify_test.go b/cmd/frpc/sub/verify_test.go new file mode 100644 index 00000000..b93db23b --- /dev/null +++ b/cmd/frpc/sub/verify_test.go @@ -0,0 +1,67 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sub + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/policy/security" +) + +func TestVerifyClientConfigFeatureGates(t *testing.T) { + tests := []struct { + name string + content string + wantErr string + }{ + { + name: "VirtualNet enabled", + content: `featureGates = { VirtualNet = true } +virtualNet.address = "100.86.0.4/24" +`, + }, + { + name: "VirtualNet disabled", + content: `featureGates = { VirtualNet = false } +virtualNet.address = "100.86.0.4/24" +`, + wantErr: "VirtualNet feature is not enabled", + }, + { + name: "unknown feature gate", + content: `featureGates = { UnknownFeature = true }`, + wantErr: "unrecognized feature gate: UnknownFeature", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + configFile := filepath.Join(t.TempDir(), "frpc.toml") + require.NoError(t, os.WriteFile(configFile, []byte(tc.content), 0o600)) + + warning, err := verifyClientConfig(configFile, true, security.NewUnsafeFeatures(nil)) + require.NoError(t, warning) + if tc.wantErr == "" { + require.NoError(t, err) + return + } + require.ErrorContains(t, err, tc.wantErr) + }) + } +} diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go index 77004317..28409c4b 100644 --- a/pkg/config/v1/validation/client.go +++ b/pkg/config/v1/validation/client.go @@ -51,14 +51,51 @@ func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) ( } func validateFeatureGates(c *v1.ClientCommonConfig) (Warning, error) { + gates := featuregate.NewFeatureGate() + if err := gates.SetFromMap(c.FeatureGates); err != nil { + return nil, err + } + if c.VirtualNet.Address != "" { - if !featuregate.Enabled(featuregate.VirtualNet) { + if !gates.Enabled(featuregate.VirtualNet) { return nil, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag") } } return nil, nil } +// ClientConfigRequirements describes runtime capabilities needed by a client configuration. +type ClientConfigRequirements struct { + VirtualNet bool +} + +// GetClientConfigRequirements returns the runtime capabilities needed by a client configuration. +func GetClientConfigRequirements( + common *v1.ClientCommonConfig, + proxyCfgs []v1.ProxyConfigurer, + visitorCfgs []v1.VisitorConfigurer, +) ClientConfigRequirements { + requirements := ClientConfigRequirements{} + if common != nil && common.VirtualNet.Address != "" { + requirements.VirtualNet = true + } + for _, cfg := range proxyCfgs { + if cfg.GetBaseConfig().Plugin.Type == v1.PluginVirtualNet { + requirements.VirtualNet = true + break + } + } + if !requirements.VirtualNet { + for _, cfg := range visitorCfgs { + if cfg.GetBaseConfig().Plugin.Type == v1.VisitorPluginVirtualNet { + requirements.VirtualNet = true + break + } + } + } + return requirements +} + func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, error) { var errs error if !slices.Contains(SupportedAuthMethods, c.Method) { diff --git a/pkg/config/v1/validation/client_test.go b/pkg/config/v1/validation/client_test.go new file mode 100644 index 00000000..1370e890 --- /dev/null +++ b/pkg/config/v1/validation/client_test.go @@ -0,0 +1,140 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validation + +import ( + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/policy/featuregate" + "github.com/fatedier/frp/pkg/policy/security" +) + +func validateClientFeatureGates(t *testing.T, gates map[string]bool, virtualNetAddress string) error { + t.Helper() + + cfg := &v1.ClientCommonConfig{ + FeatureGates: gates, + VirtualNet: v1.VirtualNetConfig{ + Address: virtualNetAddress, + }, + } + require.NoError(t, cfg.Complete()) + + _, err := NewConfigValidator(security.NewUnsafeFeatures(nil)).ValidateClientCommonConfig(cfg) + return err +} + +func TestValidateClientFeatureGates(t *testing.T) { + tests := []struct { + name string + featureGates map[string]bool + virtualNetAddress string + wantErr string + }{ + { + name: "VirtualNet enabled", + featureGates: map[string]bool{"VirtualNet": true}, + virtualNetAddress: "100.86.0.4/24", + }, + { + name: "VirtualNet explicitly disabled", + featureGates: map[string]bool{"VirtualNet": false}, + virtualNetAddress: "100.86.0.4/24", + wantErr: "VirtualNet feature is not enabled", + }, + { + name: "VirtualNet disabled by default", + virtualNetAddress: "100.86.0.4/24", + wantErr: "VirtualNet feature is not enabled", + }, + { + name: "unknown feature gate", + featureGates: map[string]bool{"UnknownFeature": true}, + wantErr: "unrecognized feature gate: UnknownFeature", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := validateClientFeatureGates(t, tc.featureGates, tc.virtualNetAddress) + if tc.wantErr == "" { + require.NoError(t, err) + return + } + require.ErrorContains(t, err, tc.wantErr) + }) + } +} + +func TestGetClientConfigRequirements(t *testing.T) { + virtualNetProxy := &v1.STCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + ProxyBackend: v1.ProxyBackend{ + Plugin: v1.TypedClientPluginOptions{Type: v1.PluginVirtualNet}, + }, + }, + } + virtualNetVisitor := &v1.STCPVisitorConfig{ + VisitorBaseConfig: v1.VisitorBaseConfig{ + Plugin: v1.TypedVisitorPluginOptions{Type: v1.VisitorPluginVirtualNet}, + }, + } + + tests := []struct { + name string + common *v1.ClientCommonConfig + proxies []v1.ProxyConfigurer + visitors []v1.VisitorConfigurer + wantVNet bool + }{ + {name: "no requirements"}, + { + name: "common VirtualNet address", + common: &v1.ClientCommonConfig{VirtualNet: v1.VirtualNetConfig{Address: "100.86.0.4/24"}}, + wantVNet: true, + }, + {name: "VirtualNet proxy", proxies: []v1.ProxyConfigurer{virtualNetProxy}, wantVNet: true}, + {name: "VirtualNet visitor", visitors: []v1.VisitorConfigurer{virtualNetVisitor}, wantVNet: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := GetClientConfigRequirements(tc.common, tc.proxies, tc.visitors) + require.Equal(t, tc.wantVNet, got.VirtualNet) + }) + } +} + +func TestValidateClientFeatureGatesAreConfigScoped(t *testing.T) { + defaultGatesBefore := featuregate.DefaultFeatureGates.String() + + require.NoError(t, validateClientFeatureGates( + t, + map[string]bool{"VirtualNet": true}, + "100.86.0.4/24", + )) + require.Equal(t, defaultGatesBefore, featuregate.DefaultFeatureGates.String()) + + err := validateClientFeatureGates( + t, + map[string]bool{"VirtualNet": false}, + "100.86.0.4/24", + ) + require.ErrorContains(t, err, "VirtualNet feature is not enabled") + require.Equal(t, defaultGatesBefore, featuregate.DefaultFeatureGates.String()) +} From d1928f968938e4f1b3b72fd12d581342e57a5030 Mon Sep 17 00:00:00 2001 From: Yarden Shoham Date: Wed, 5 Aug 2026 05:52:16 +0300 Subject: [PATCH 15/23] remove retired Go Report Card badge (#5467) --- README_zh.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index 0f0da0eb..600cd4fd 100644 --- a/README_zh.md +++ b/README_zh.md @@ -2,7 +2,6 @@ [![Build Status](https://circleci.com/gh/fatedier/frp.svg?style=shield)](https://circleci.com/gh/fatedier/frp) [![GitHub release](https://img.shields.io/github/tag/fatedier/frp.svg?label=release)](https://github.com/fatedier/frp/releases) -[![Go Report Card](https://goreportcard.com/badge/github.com/fatedier/frp)](https://goreportcard.com/report/github.com/fatedier/frp) [![GitHub Releases Stats](https://img.shields.io/github/downloads/fatedier/frp/total.svg?logo=github)](https://somsubhra.github.io/github-release-stats/?username=fatedier&repository=frp) [README](README.md) | [中文文档](README_zh.md) From 5b68148f1141764ca90390c146294d60529a8173 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 9 Aug 2026 15:31:11 +0800 Subject: [PATCH 16/23] udp: handle closed forwarding channel (#5470) --- pkg/proto/udp/udp.go | 10 +++++++--- pkg/proto/udp/udp_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/pkg/proto/udp/udp.go b/pkg/proto/udp/udp.go index 70b51900..ec3bd94f 100644 --- a/pkg/proto/udp/udp.go +++ b/pkg/proto/udp/udp.go @@ -63,9 +63,13 @@ func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh // NewUDPPacket copies buf[:n], so the read buffer can be reused udpMsg := NewUDPPacket(buf[:n], nil, remoteAddr) - select { - case sendCh <- udpMsg: - default: + if err = errors.PanicToError(func() { + select { + case sendCh <- udpMsg: + default: + } + }); err != nil { + return } } } diff --git a/pkg/proto/udp/udp_test.go b/pkg/proto/udp/udp_test.go index 1a7f0091..b959a26a 100644 --- a/pkg/proto/udp/udp_test.go +++ b/pkg/proto/udp/udp_test.go @@ -1,9 +1,13 @@ package udp import ( + "net" "testing" + "time" "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/msg" ) func TestUdpPacket(t *testing.T) { @@ -16,3 +20,33 @@ func TestUdpPacket(t *testing.T) { require.NoError(err) require.EqualValues(buf, newBuf) } + +func TestForwardUserConnReturnsWhenSendChannelIsClosed(t *testing.T) { + listener, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) + require.NoError(t, err) + t.Cleanup(func() { _ = listener.Close() }) + + readCh := make(chan *msg.UDPPacket) + sendCh := make(chan *msg.UDPPacket) + close(sendCh) + t.Cleanup(func() { close(readCh) }) + + done := make(chan struct{}) + go func() { + ForwardUserConn(listener, readCh, sendCh, 1500) + close(done) + }() + + sender, err := net.DialUDP("udp4", nil, listener.LocalAddr().(*net.UDPAddr)) + require.NoError(t, err) + t.Cleanup(func() { _ = sender.Close() }) + + _, err = sender.Write([]byte("trigger")) + require.NoError(t, err) + + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("ForwardUserConn did not return after sending to a closed channel") + } +} From f666d97b64d6902ce15dcd329087cb1aadaf76d3 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 9 Aug 2026 17:30:39 +0800 Subject: [PATCH 17/23] limit: clamp bandwidth limiter burst (#5471) --- client/proxy/proxy.go | 2 +- pkg/util/limit/limiter.go | 37 +++++++++++++++++++ pkg/util/limit/limiter_test.go | 65 ++++++++++++++++++++++++++++++++++ pkg/util/limit/reader.go | 6 ++++ pkg/util/limit/writer.go | 7 ++++ server/proxy/proxy.go | 2 +- 6 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 pkg/util/limit/limiter.go create mode 100644 pkg/util/limit/limiter_test.go diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index 5af56599..ece2f935 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -66,7 +66,7 @@ func NewProxy( var limiter *rate.Limiter limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes() if limitBytes > 0 && pxyConf.GetBaseConfig().Transport.BandwidthLimitMode == types.BandwidthLimitModeClient { - limiter = rate.NewLimiter(rate.Limit(float64(limitBytes)), int(limitBytes)) + limiter = limit.NewBandwidthLimiter(limitBytes) } baseProxy := BaseProxy{ diff --git a/pkg/util/limit/limiter.go b/pkg/util/limit/limiter.go new file mode 100644 index 00000000..43ba7434 --- /dev/null +++ b/pkg/util/limit/limiter.go @@ -0,0 +1,37 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package limit + +import ( + "fmt" + + "golang.org/x/time/rate" +) + +// NewBandwidthLimiter creates a limiter whose rate preserves the configured +// byte limit while keeping the burst representable as an int on all targets. +func NewBandwidthLimiter(bytes int64) *rate.Limiter { + if bytes <= 0 { + return nil + } + + maxInt := int64(^uint(0) >> 1) + burst := min(bytes, maxInt) + return rate.NewLimiter(rate.Limit(float64(bytes)), int(burst)) +} + +func invalidBurstError(burst int) error { + return fmt.Errorf("invalid limiter burst: %d", burst) +} diff --git a/pkg/util/limit/limiter_test.go b/pkg/util/limit/limiter_test.go new file mode 100644 index 00000000..f245bcfc --- /dev/null +++ b/pkg/util/limit/limiter_test.go @@ -0,0 +1,65 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package limit + +import ( + "bytes" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/time/rate" +) + +func TestNewBandwidthLimiterClampsBurstToTargetInt(t *testing.T) { + const bytesPerSecond = int64(1 << 31) + + limiter := NewBandwidthLimiter(bytesPerSecond) + require.NotNil(t, limiter) + + wantBurst := bytesPerSecond + maxInt := int64(^uint(0) >> 1) + if wantBurst > maxInt { + wantBurst = maxInt + } + require.Equal(t, int(wantBurst), limiter.Burst()) + require.Equal(t, rate.Limit(float64(bytesPerSecond)), limiter.Limit()) +} + +func TestNewBandwidthLimiterDisablesNonPositiveLimit(t *testing.T) { + require.Nil(t, NewBandwidthLimiter(0)) + require.Nil(t, NewBandwidthLimiter(-1)) +} + +func TestReaderAndWriterRejectInvalidBurst(t *testing.T) { + for _, burst := range []int{0, -1} { + t.Run("reader/"+strconv.Itoa(burst), func(t *testing.T) { + reader := NewReader(strings.NewReader("payload"), rate.NewLimiter(rate.Limit(1), burst)) + n, err := reader.Read(make([]byte, 1)) + require.Zero(t, n) + require.EqualError(t, err, "invalid limiter burst: "+strconv.Itoa(burst)) + }) + + t.Run("writer/"+strconv.Itoa(burst), func(t *testing.T) { + var dst bytes.Buffer + writer := NewWriter(&dst, rate.NewLimiter(rate.Limit(1), burst)) + n, err := writer.Write([]byte("payload")) + require.Zero(t, n) + require.EqualError(t, err, "invalid limiter burst: "+strconv.Itoa(burst)) + require.Empty(t, dst.Bytes()) + }) + } +} diff --git a/pkg/util/limit/reader.go b/pkg/util/limit/reader.go index efa828f4..eccca5a5 100644 --- a/pkg/util/limit/reader.go +++ b/pkg/util/limit/reader.go @@ -35,6 +35,12 @@ func NewReader(r io.Reader, limiter *rate.Limiter) *Reader { func (r *Reader) Read(p []byte) (n int, err error) { b := r.limiter.Burst() + if b <= 0 { + if len(p) == 0 { + return 0, nil + } + return 0, invalidBurstError(b) + } if b < len(p) { p = p[:b] } diff --git a/pkg/util/limit/writer.go b/pkg/util/limit/writer.go index 5256d1e2..56357228 100644 --- a/pkg/util/limit/writer.go +++ b/pkg/util/limit/writer.go @@ -34,8 +34,15 @@ func NewWriter(w io.Writer, limiter *rate.Limiter) *Writer { } func (w *Writer) Write(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + var nn int b := w.limiter.Burst() + if b <= 0 { + return 0, invalidBurstError(b) + } for { end := len(p) if end == 0 { diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index e0dbb7f2..f4e75f0a 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -536,7 +536,7 @@ func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) { var limiter *rate.Limiter limitBytes := configurer.GetBaseConfig().Transport.BandwidthLimit.Bytes() if limitBytes > 0 && configurer.GetBaseConfig().Transport.BandwidthLimitMode == types.BandwidthLimitModeServer { - limiter = rate.NewLimiter(rate.Limit(float64(limitBytes)), int(limitBytes)) + limiter = limit.NewBandwidthLimiter(limitBytes) } basePxy := BaseProxy{ From f6688e2a0d7fbd6ff3fd81f04042946c79e7e9ec Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 9 Aug 2026 18:39:21 +0800 Subject: [PATCH 18/23] client: reject invalid work connection addresses (#5472) --- client/proxy/proxy.go | 25 ++++++++++++++++---- client/proxy/proxy_test.go | 47 ++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 client/proxy/proxy_test.go diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index ece2f935..e982c03b 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -174,6 +174,26 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor xl.Tracef("handle tcp work connection, useEncryption: %t, useCompression: %t", baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression) + var srcAddr, dstAddr *net.TCPAddr + if m.SrcAddr != "" && m.SrcPort != 0 { + if m.DstAddr == "" { + m.DstAddr = "127.0.0.1" + } + var err error + srcAddr, err = net.ResolveTCPAddr("tcp", net.JoinHostPort(m.SrcAddr, strconv.Itoa(int(m.SrcPort)))) + if err != nil { + xl.Warnf("resolve source address [%s] error: %v", m.SrcAddr, err) + _ = workConn.Close() + return + } + dstAddr, err = net.ResolveTCPAddr("tcp", net.JoinHostPort(m.DstAddr, strconv.Itoa(int(m.DstPort)))) + if err != nil { + xl.Warnf("resolve destination address [%s] error: %v", m.DstAddr, err) + _ = workConn.Close() + return + } + } + remote, recycleFn, err := pxy.wrapWorkConn(workConn, encKey) if err != nil { xl.Errorf("wrap work connection: %v", err) @@ -183,11 +203,6 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor // check if we need to send proxy protocol info var connInfo plugin.ConnectionInfo if m.SrcAddr != "" && m.SrcPort != 0 { - if m.DstAddr == "" { - m.DstAddr = "127.0.0.1" - } - srcAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.SrcAddr, strconv.Itoa(int(m.SrcPort)))) - dstAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.DstAddr, strconv.Itoa(int(m.DstPort)))) connInfo.SrcAddr = srcAddr connInfo.DstAddr = dstAddr } diff --git a/client/proxy/proxy_test.go b/client/proxy/proxy_test.go new file mode 100644 index 00000000..c51c6997 --- /dev/null +++ b/client/proxy/proxy_test.go @@ -0,0 +1,47 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !frps + +package proxy + +import ( + "io" + "net" + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/util/xlog" +) + +func TestHandleTCPWorkConnectionRejectsInvalidAddress(t *testing.T) { + workConn, peerConn := net.Pipe() + defer peerConn.Close() + + pxy := &BaseProxy{ + baseCfg: &v1.ProxyBaseConfig{}, + xl: xlog.New(), + } + pxy.HandleTCPWorkConnection(workConn, &msg.StartWorkConn{ + SrcAddr: "[", + SrcPort: 1, + }, nil) + + buffer := make([]byte, 1) + _, err := peerConn.Read(buffer) + require.ErrorIs(t, err, io.EOF) +} diff --git a/go.mod b/go.mod index c1ca1917..30c106d5 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/gomega v1.36.3 github.com/pelletier/go-toml/v2 v2.2.0 - github.com/pires/go-proxyproto v0.7.0 + github.com/pires/go-proxyproto v0.15.0 github.com/prometheus/client_golang v1.19.1 github.com/quic-go/quic-go v0.60.0 github.com/rodaine/table v1.2.0 diff --git a/go.sum b/go.sum index 81758d50..238c5ae2 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= -github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= +github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI= +github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= From 223b44336cba7cd21e6dd05ea3b991768c8139fa Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 9 Aug 2026 19:12:12 +0800 Subject: [PATCH 19/23] ssh: serialize tunnel channel writes (#5473) --- pkg/ssh/server.go | 5 +++++ pkg/ssh/server_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/pkg/ssh/server.go b/pkg/ssh/server.go index c042b9c1..8a69fc30 100644 --- a/pkg/ssh/server.go +++ b/pkg/ssh/server.go @@ -70,6 +70,7 @@ type TunnelServer struct { sshConn *ssh.ServerConn sc *ssh.ServerConfig firstChannel ssh.Channel + firstChannelMu sync.Mutex vc *virtual.Client peerServerListener *netpkg.InternalListener @@ -191,6 +192,8 @@ func (s *TunnelServer) Run() error { } func (s *TunnelServer) writeToClient(data string) { + s.firstChannelMu.Lock() + defer s.firstChannelMu.Unlock() if s.firstChannel == nil { return } @@ -304,9 +307,11 @@ func (s *TunnelServer) handleNewChannel(channel ssh.NewChannel, extraPayloadCh c if err != nil { return } + s.firstChannelMu.Lock() if s.firstChannel == nil { s.firstChannel = ch } + s.firstChannelMu.Unlock() go s.keepAlive(ch) for req := range reqs { diff --git a/pkg/ssh/server_test.go b/pkg/ssh/server_test.go index 78421a55..56b1ba5b 100644 --- a/pkg/ssh/server_test.go +++ b/pkg/ssh/server_test.go @@ -16,7 +16,11 @@ package ssh import ( "encoding/binary" + "io" + "sync" + "sync/atomic" "testing" + "time" "github.com/stretchr/testify/require" cryptossh "golang.org/x/crypto/ssh" @@ -69,3 +73,43 @@ func TestParseExecPayloadRejectsMalformedPayloads(t *testing.T) { }) } } + +type trackingChannel struct { + active atomic.Int32 + concurrent atomic.Bool +} + +func (c *trackingChannel) Read([]byte) (int, error) { return 0, io.EOF } + +func (c *trackingChannel) Write(p []byte) (int, error) { + if c.active.Add(1) != 1 { + c.concurrent.Store(true) + } + time.Sleep(time.Millisecond) + c.active.Add(-1) + return len(p), nil +} + +func (c *trackingChannel) Close() error { return nil } +func (c *trackingChannel) CloseWrite() error { return nil } +func (c *trackingChannel) SendRequest(string, bool, []byte) (bool, error) { return false, nil } +func (c *trackingChannel) Stderr() io.ReadWriter { return nil } + +func TestWriteToClientSerializesChannelWrites(t *testing.T) { + channel := &trackingChannel{} + s := &TunnelServer{firstChannel: channel} + start := make(chan struct{}) + var wg sync.WaitGroup + for range 8 { + wg.Go(func() { + <-start + s.writeToClient("message") + }) + } + close(start) + wg.Wait() + + if channel.concurrent.Load() { + t.Fatal("channel writes were concurrent") + } +} From a6a782bed4feb783b39d9e90c59a3dbd0ee885b9 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 9 Aug 2026 22:52:30 +0800 Subject: [PATCH 20/23] config: reject case-insensitive subdomain domains (#5474) --- pkg/config/v1/validation/proxy.go | 6 +- pkg/config/v1/validation/proxy_test.go | 76 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 pkg/config/v1/validation/proxy_test.go diff --git a/pkg/config/v1/validation/proxy.go b/pkg/config/v1/validation/proxy.go index 744620f0..5d430e6b 100644 --- a/pkg/config/v1/validation/proxy.go +++ b/pkg/config/v1/validation/proxy.go @@ -79,9 +79,11 @@ func validateDomainConfigForClient(c *v1.DomainConfig) error { } func validateDomainConfigForServer(c *v1.DomainConfig, s *v1.ServerConfig) error { + subDomainHost := strings.ToLower(s.SubDomainHost) for _, domain := range c.CustomDomains { - if s.SubDomainHost != "" && len(strings.Split(s.SubDomainHost, ".")) < len(strings.Split(domain, ".")) { - if strings.HasSuffix(domain, "."+s.SubDomainHost) { + canonicalDomain := strings.ToLower(domain) + if subDomainHost != "" && len(strings.Split(subDomainHost, ".")) < len(strings.Split(canonicalDomain, ".")) { + if strings.HasSuffix(canonicalDomain, "."+subDomainHost) { return fmt.Errorf("custom domain [%s] should not belong to subdomain host [%s]", domain, s.SubDomainHost) } } diff --git a/pkg/config/v1/validation/proxy_test.go b/pkg/config/v1/validation/proxy_test.go new file mode 100644 index 00000000..b9411dc8 --- /dev/null +++ b/pkg/config/v1/validation/proxy_test.go @@ -0,0 +1,76 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validation + +import ( + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func TestValidateDomainConfigForServerRejectsSubdomainHostCaseInsensitively(t *testing.T) { + tests := []struct { + name string + subDomainHost string + customDomain string + wantErr bool + }{ + { + name: "lowercase subdomain", + subDomainHost: "frp.example.com", + customDomain: "victim.frp.example.com", + wantErr: true, + }, + { + name: "mixed case custom domain", + subDomainHost: "frp.example.com", + customDomain: "victim.FRP.example.com", + wantErr: true, + }, + { + name: "mixed case wildcard domain", + subDomainHost: "frp.example.com", + customDomain: "*.FRP.example.com", + wantErr: true, + }, + { + name: "mixed case subdomain host", + subDomainHost: "FRP.Example.Com", + customDomain: "victim.frp.example.com", + wantErr: true, + }, + { + name: "external domain", + subDomainHost: "frp.example.com", + customDomain: "victim.example.net", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateDomainConfigForServer( + &v1.DomainConfig{CustomDomains: []string{tt.customDomain}}, + &v1.ServerConfig{SubDomainHost: tt.subDomainHost}, + ) + if tt.wantErr { + require.ErrorContains(t, err, "should not belong to subdomain host") + return + } + require.NoError(t, err) + }) + } +} From 71a2bf30f93c7b542096a6d2d927c945740db785 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 9 Aug 2026 23:04:09 +0800 Subject: [PATCH 21/23] docs: add release note for domain validation fix (#5475) --- Release.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Release.md b/Release.md index 4716e374..33fc0aea 100644 --- a/Release.md +++ b/Release.md @@ -6,3 +6,4 @@ * Fixed a server panic and remote denial of service caused by a client sending a negative `pool_count`. Negative values are now rejected before work-connection pool resources are allocated. * Fixed `frpc verify` ignoring configured `featureGates`, which caused VirtualNet configurations to be rejected even when the feature was enabled. +* Fixed a case-insensitive validation bypass that allowed `customDomains` under the configured `subDomainHost` to be registered using mixed-case domain names. From da04e1e07a76487fc9e19334a04a1cb9414f1de3 Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 12 Aug 2026 23:56:00 +0800 Subject: [PATCH 22/23] log: improve prefix handling (#5489) --- go.mod | 2 +- go.sum | 4 +- pkg/config/v1/validation/name.go | 48 +++++++++++++++++ pkg/config/v1/validation/name_test.go | 48 +++++++++++++++++ pkg/util/xlog/xlog.go | 10 ++-- pkg/util/xlog/xlog_test.go | 76 +++++++++++++++++++++++++++ server/service.go | 4 ++ server/service_test.go | 19 +++++++ 8 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 pkg/config/v1/validation/name.go create mode 100644 pkg/config/v1/validation/name_test.go create mode 100644 pkg/util/xlog/xlog_test.go diff --git a/go.mod b/go.mod index 30c106d5..9e02c333 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.0 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/coreos/go-oidc/v3 v3.18.0 - github.com/fatedier/golib v0.8.1 + github.com/fatedier/golib v0.8.2 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.0 diff --git a/go.sum b/go.sum index 238c5ae2..30641d78 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatedier/golib v0.8.1 h1:pHcIu0zAcZ6VTkO1dW/meelCGN5nem52DKCBY7cUvyA= -github.com/fatedier/golib v0.8.1/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw= +github.com/fatedier/golib v0.8.2 h1:02n2Dg7KJ7rR7p7n4/6hBUjaLQf2J7EiHYZQsgGTvww= +github.com/fatedier/golib v0.8.2/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw= github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE= github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= diff --git a/pkg/config/v1/validation/name.go b/pkg/config/v1/validation/name.go new file mode 100644 index 00000000..ce03de88 --- /dev/null +++ b/pkg/config/v1/validation/name.go @@ -0,0 +1,48 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validation + +import ( + "fmt" + "unicode" + "unicode/utf8" +) + +const ( + // MaxRunIDLength is the maximum number of bytes accepted for a control run ID. + MaxRunIDLength = 64 +) + +func validateIdentifier(value, kind string, maxLength int) error { + if value == "" { + return fmt.Errorf("%s cannot be empty", kind) + } + if len(value) > maxLength { + return fmt.Errorf("%s is too long: length %d exceeds maximum %d", kind, len(value), maxLength) + } + if !utf8.ValidString(value) { + return fmt.Errorf("%s must be valid UTF-8", kind) + } + for _, r := range value { + if !unicode.IsPrint(r) { + return fmt.Errorf("%s contains non-printable character", kind) + } + } + return nil +} + +func ValidateRunID(runID string) error { + return validateIdentifier(runID, "run id", MaxRunIDLength) +} diff --git a/pkg/config/v1/validation/name_test.go b/pkg/config/v1/validation/name_test.go new file mode 100644 index 00000000..1095e78f --- /dev/null +++ b/pkg/config/v1/validation/name_test.go @@ -0,0 +1,48 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validation + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidateIdentifiers(t *testing.T) { + tests := []struct { + name string + validate func(string) error + value string + wantError string + }{ + {name: "run id accepts printable values", validate: ValidateRunID, value: "run-%1000s-中文"}, + {name: "run id rejects empty", validate: ValidateRunID, wantError: "cannot be empty"}, + {name: "run id rejects control character", validate: ValidateRunID, value: "run\nforged", wantError: "non-printable"}, + {name: "run id rejects invalid utf8", validate: ValidateRunID, value: string([]byte{0xff}), wantError: "valid UTF-8"}, + {name: "run id rejects excessive length", validate: ValidateRunID, value: strings.Repeat("a", MaxRunIDLength+1), wantError: "too long"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.validate(tt.value) + if tt.wantError == "" { + require.NoError(t, err) + return + } + require.ErrorContains(t, err, tt.wantError) + }) + } +} diff --git a/pkg/util/xlog/xlog.go b/pkg/util/xlog/xlog.go index f8f4116a..1021d44a 100644 --- a/pkg/util/xlog/xlog.go +++ b/pkg/util/xlog/xlog.go @@ -96,21 +96,21 @@ func (l *Logger) Spawn() *Logger { } func (l *Logger) Errorf(format string, v ...any) { - log.Logger.Errorf(l.prefixString+format, v...) + log.Logger.WithPrefix(l.prefixString).Errorf(format, v...) } func (l *Logger) Warnf(format string, v ...any) { - log.Logger.Warnf(l.prefixString+format, v...) + log.Logger.WithPrefix(l.prefixString).Warnf(format, v...) } func (l *Logger) Infof(format string, v ...any) { - log.Logger.Infof(l.prefixString+format, v...) + log.Logger.WithPrefix(l.prefixString).Infof(format, v...) } func (l *Logger) Debugf(format string, v ...any) { - log.Logger.Debugf(l.prefixString+format, v...) + log.Logger.WithPrefix(l.prefixString).Debugf(format, v...) } func (l *Logger) Tracef(format string, v ...any) { - log.Logger.Tracef(l.prefixString+format, v...) + log.Logger.WithPrefix(l.prefixString).Tracef(format, v...) } diff --git a/pkg/util/xlog/xlog_test.go b/pkg/util/xlog/xlog_test.go new file mode 100644 index 00000000..13d006ef --- /dev/null +++ b/pkg/util/xlog/xlog_test.go @@ -0,0 +1,76 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package xlog + +import ( + "bytes" + "testing" + + goliblog "github.com/fatedier/golib/log" + "github.com/stretchr/testify/require" + + frplog "github.com/fatedier/frp/pkg/util/log" +) + +func TestPrefixIsNotPartOfFormatString(t *testing.T) { + tests := []struct { + name string + log func(*Logger) + }{ + {name: "error", log: func(xl *Logger) { xl.Errorf("value [%s]", "ok") }}, + {name: "warn", log: func(xl *Logger) { xl.Warnf("value [%s]", "ok") }}, + {name: "info", log: func(xl *Logger) { xl.Infof("value [%s]", "ok") }}, + {name: "debug", log: func(xl *Logger) { xl.Debugf("value [%s]", "ok") }}, + {name: "trace", log: func(xl *Logger) { xl.Tracef("value [%s]", "ok") }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output := captureLogs(t) + + tt.log(New().AppendPrefix("%1000000s")) + + require.Contains(t, output.String(), "[%1000000s] value [ok]") + require.Less(t, output.Len(), 1024) + }) + } +} + +func TestFormattingSemanticsArePreserved(t *testing.T) { + output := captureLogs(t) + xl := New().AppendPrefix("run") + + xl.Infof("%[2]s %[1]s", "first", "second") + xl.Infof("100% complete") + + require.Contains(t, output.String(), "[run] second first") + require.Contains(t, output.String(), "[run] 100% complete") +} + +func captureLogs(t *testing.T) *bytes.Buffer { + t.Helper() + + output := bytes.NewBuffer(nil) + oldLogger := frplog.Logger + frplog.Logger = goliblog.New( + goliblog.WithOutput(output), + goliblog.WithLevel(goliblog.TraceLevel), + goliblog.WithCaller(false), + ) + t.Cleanup(func() { + frplog.Logger = oldLogger + }) + return output +} diff --git a/server/service.go b/server/service.go index 7aba085e..82883bb2 100644 --- a/server/service.go +++ b/server/service.go @@ -35,6 +35,7 @@ import ( "github.com/fatedier/frp/pkg/auth" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" modelmetrics "github.com/fatedier/frp/pkg/metrics" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/nathole" @@ -791,6 +792,9 @@ func (svr *Service) RegisterControl( return nil, err } } + if err := validation.ValidateRunID(loginMsg.RunID); err != nil { + return nil, fmt.Errorf("invalid run id: %w", err) + } ctx := netpkg.NewContextFromConn(ctlConn) xl := xlog.FromContextSafe(ctx) diff --git a/server/service_test.go b/server/service_test.go index e35f11b9..d4fcbe77 100644 --- a/server/service_test.go +++ b/server/service_test.go @@ -17,10 +17,12 @@ package server import ( "context" "errors" + "fmt" "math" "net" "net/http" "runtime" + "strings" "sync" "sync/atomic" "testing" @@ -31,6 +33,7 @@ import ( "github.com/fatedier/frp/pkg/auth" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/msg" plugin "github.com/fatedier/frp/pkg/plugin/server" "github.com/fatedier/frp/pkg/proto/wire" @@ -638,6 +641,22 @@ func TestServiceRegisterControlRejectsInvalidCodecSelection(t *testing.T) { } } +func TestServiceRegisterControlRejectsInvalidRunID(t *testing.T) { + for _, runID := range []string{ + "run\nforged", + strings.Repeat("a", validation.MaxRunIDLength+1), + } { + t.Run(fmt.Sprintf("run_id_%d", len(runID)), func(t *testing.T) { + svr := newControlTestService(t) + conn := newDeadlineReadConn() + msgConn := msg.NewConn(conn, msg.NewV1ReadWriter(conn)) + ctl, err := svr.RegisterControl(msgConn, &msg.Login{RunID: runID}, true, wire.ProtocolV1, "") + require.Nil(t, ctl) + require.ErrorContains(t, err, "invalid run id") + }) + } +} + func TestServiceRegisterControlPoolCountBoundaries(t *testing.T) { for _, tc := range []struct { name string From 6c8a8d0a97d03b44e9528d30b30c70cb9d61b405 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 13 Aug 2026 01:12:34 +0800 Subject: [PATCH 23/23] web: patch vulnerable transitive dependencies (#5490) --- web/package-lock.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 95ef7757..d31d4b03 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2654,9 +2654,9 @@ "license": "MIT" }, "node_modules/@vue/test-utils/node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -3075,9 +3075,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { @@ -5682,9 +5682,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -5811,9 +5811,9 @@ "license": "MIT" }, "node_modules/npm-run-all/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": {